<?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: Milan Jovanović</title>
    <description>The latest articles on DEV Community by Milan Jovanović (@milanjovanovictech).</description>
    <link>https://dev.to/milanjovanovictech</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1143226%2F5523f4b1-da5d-4c9d-8855-ed5a1d8e3548.png</url>
      <title>DEV Community: Milan Jovanović</title>
      <link>https://dev.to/milanjovanovictech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/milanjovanovictech"/>
    <language>en</language>
    <item>
      <title>Building a Custom Domain Events Dispatcher in .NET</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 24 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/building-a-custom-domain-events-dispatcher-in-net-p5</link>
      <guid>https://dev.to/milanjovanovictech/building-a-custom-domain-events-dispatcher-in-net-p5</guid>
      <description>&lt;p&gt;Domain events are a powerful way to decouple parts of your system. Instead of tightly coupling your logic, you can publish events and have other parts of your code subscribe to those events. This pattern is especially valuable in &lt;a href="https://en.wikipedia.org/wiki/Domain-driven_design" rel="noopener noreferrer"&gt;Domain-Driven Design&lt;/a&gt; (DDD) where business logic should remain focused and cohesive.&lt;/p&gt;

&lt;p&gt;In this article, we'll walk through how to implement a lightweight, custom domain event dispatcher in .NET. The core dispatching logic should not depend on third-party libraries.&lt;/p&gt;

&lt;p&gt;We'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why you might want to use publish-subscribe in your application&lt;/li&gt;
&lt;li&gt;How to define basic domain event abstractions&lt;/li&gt;
&lt;li&gt;How to implement and register handlers&lt;/li&gt;
&lt;li&gt;How to build a domain events dispatcher&lt;/li&gt;
&lt;li&gt;Trade-offs and when to consider other options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Domain Events Matter
&lt;/h2&gt;

&lt;p&gt;Before diving into implementation, let's understand the problem domain events solve. Consider this tightly coupled code:&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&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;RegisterUser&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;password&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;user&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;User&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="n"&gt;password&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;_userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Directly coupled to email service&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&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="c1"&gt;// Directly coupled to analytics&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_analyticsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrackUserRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// What if we need to add more features?&lt;/span&gt;
        &lt;span class="c1"&gt;// This method will keep growing...&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;With domain events, we can decouple this:&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&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;RegisterUser&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;password&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;user&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;User&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="n"&gt;password&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;_userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Publish event - let other parts of the system react&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_domainEventsDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&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;UserRegisteredDomainEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&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="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;Now the &lt;code&gt;UserService&lt;/code&gt; focuses solely on user registration, while other concerns are handled through event handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Abstractions
&lt;/h2&gt;

&lt;p&gt;Let's start by defining two simple interfaces that form the foundation of our event system:&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="c1"&gt;// Marker interface for all domain events.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDomainEvent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We could add common properties here like:&lt;/span&gt;
    &lt;span class="c1"&gt;// DateTime OccurredAt { get; }&lt;/span&gt;
    &lt;span class="c1"&gt;// Guid EventId { get; }&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Generic interface for handling domain events.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDomainEvent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;domainEvent&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;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&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;This design gives us type safety through generic constraints while keeping publishers and handlers completely decoupled. You can add new events or handlers without touching existing code, and everything remains easily testable in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Sample Handlers
&lt;/h2&gt;

&lt;p&gt;Let's add some sample handlers that demonstrate how different parts of your system can react to the same event:&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="c1"&gt;// Handles sending welcome emails when users register&lt;/span&gt;
&lt;span class="k"&gt;internal&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;SendWelcomeEmailHandler&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;emailService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserRegisteredDomainEvent&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;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UserRegisteredDomainEvent&lt;/span&gt; &lt;span class="n"&gt;domainEvent&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;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Send welcome email&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;welcomeEmail&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;WelcomeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domainEvent&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="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&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;emailService&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;welcomeEmail&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Handles analytics tracking for new user registrations&lt;/span&gt;
&lt;span class="k"&gt;internal&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;TrackUserRegistrationHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IAnalyticsService&lt;/span&gt; &lt;span class="n"&gt;analyticsService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserRegisteredDomainEvent&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;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UserRegisteredDomainEvent&lt;/span&gt; &lt;span class="n"&gt;domainEvent&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;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Track registration in analytics&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;analyticsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"user_registered"&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;user_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;registration_date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisteredAt&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="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;To make this work, we need to register our handlers with the DI container.&lt;/p&gt;

&lt;p&gt;Here's how to do it manually:&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="c1"&gt;// In your Program.cs or Startup.cs&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;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserRegisteredDomainEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;SendWelcomeEmailHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;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;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserRegisteredDomainEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;TrackUserRegistrationHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can automate this registration using &lt;a href="https://www.milanjovanovic.tech/blog/improving-aspnetcore-dependency-injection-with-scrutor" rel="noopener noreferrer"&gt;&lt;strong&gt;assembly scanning with Scrutor&lt;/strong&gt;&lt;/a&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;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromAssembliesOf&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;DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssignableTo&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;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;)),&lt;/span&gt; &lt;span class="n"&gt;publicOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing is that multiple handlers can react to the same event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dispatcher (Strongly Typed)
&lt;/h2&gt;

&lt;p&gt;Now we need something to orchestrate calling the handlers. The dispatcher will take the domain events and call the appropriate handlers for each event.&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IDomainEventsDispatcher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDomainEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;domainEvents&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;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;internal&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;DomainEventsDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDomainEventsDispatcher&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;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HandlerTypeDictionary&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WrapperTypeDictionary&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="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;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDomainEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;domainEvents&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;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDomainEvent&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domainEvents&lt;/span&gt;&lt;span class="p"&gt;)&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;IServiceScope&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;domainEventType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domainEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;handlerType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HandlerTypeDictionary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;domainEventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;et&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;).&lt;/span&gt;&lt;span class="nf"&gt;MakeGenericType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;et&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlerType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;)&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;handler&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="k"&gt;continue&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;handlerWrapper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HandlerWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domainEventType&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;handlerWrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domainEvent&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="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Abstract base class for strongly-typed handler wrappers&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HandlerWrapper&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;abstract&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDomainEvent&lt;/span&gt; &lt;span class="n"&gt;domainEvent&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;cancellationToken&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;static&lt;/span&gt; &lt;span class="n"&gt;HandlerWrapper&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;domainEventType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;wrapperType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WrapperTypeDictionary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;domainEventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;et&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;HandlerWrapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;).&lt;/span&gt;&lt;span class="nf"&gt;MakeGenericType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;et&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HandlerWrapper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapperType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&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="c1"&gt;// Generic wrapper that provides strong typing for handler invocation&lt;/span&gt;
    &lt;span class="k"&gt;private&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;HandlerWrapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HandlerWrapper&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDomainEvent&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;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;handler&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;override&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;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;IDomainEvent&lt;/span&gt; &lt;span class="n"&gt;domainEvent&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&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;_handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;domainEvent&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="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 dispatcher uses a wrapper to eliminate reflection during handler execution while maintaining type safety. When we encounter a &lt;code&gt;UserRegisteredDomainEvent&lt;/code&gt;, we create a &lt;code&gt;HandlerWrapper&amp;lt;UserRegisteredDomainEvent&amp;gt;&lt;/code&gt;that holds a strongly-typed reference to &lt;code&gt;IDomainEventHandler&amp;lt;UserRegisteredDomainEvent&amp;gt;&lt;/code&gt;. The wrapper casts the generic &lt;code&gt;IDomainEvent&lt;/code&gt; to the specific event type at runtime, but the actual handler invocation uses compile-time types.&lt;/p&gt;

&lt;p&gt;This gives us the performance benefits of avoiding reflection in the hot path (handler execution) while only using reflection once during wrapper creation. The trade-off is additional complexity, but the performance gain is significant if you're dispatching many events.&lt;/p&gt;

&lt;p&gt;Don't forget to register the dispatcher with DI:&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;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDomainEventsDispatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DomainEventsDispatcher&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage Example
&lt;/h2&gt;

&lt;p&gt;Here's how to use the domain events dispatcher in your application:&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IUserService&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;IDomainEventsDispatcher&lt;/span&gt; &lt;span class="n"&gt;domainEventsDispatcher&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="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"register"&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;Register&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;RegisterUserRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Create the user&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&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;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateUserAsync&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;Email&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;Password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Publish the domain event&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;userRegisteredEvent&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;UserRegisteredDomainEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&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;await&lt;/span&gt; &lt;span class="n"&gt;domainEventsDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;userRegisteredEvent&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;Ok&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;UserId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"User registered successfully"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&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;BadRequest&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;Error&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could also &lt;a href="https://www.milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems" rel="noopener noreferrer"&gt;&lt;strong&gt;integrate domain events directly into your domain entities&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Tradeoffs
&lt;/h2&gt;

&lt;p&gt;This implementation runs entirely in-process, which has important implications. All handlers execute synchronously within the same request context, but each gets its own DI scope. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Immediate feedback&lt;/strong&gt; : If any handler fails, the exception bubbles up to the caller immediately. No silent failures or eventual consistency surprises.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Caller control&lt;/strong&gt; : The code that dispatches events decides how to handle failures — rollback transactions, retry operations, or continue despite errors. The dispatcher doesn't make these decisions for you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reliability concerns&lt;/strong&gt; : If the process crashes after some handlers succeed but before others complete, there's no automatic recovery. Events aren't persisted or retried.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For critical side effects that can't be lost, consider the &lt;a href="https://www.milanjovanovic.tech/blog/implementing-the-outbox-pattern" rel="noopener noreferrer"&gt;&lt;strong&gt;Outbox pattern&lt;/strong&gt;&lt;/a&gt;. Instead of dispatching events immediately, store them alongside your business data in the same transaction. A background service can later retry failed events, ensuring nothing gets lost. This decouples reliability from performance — your main operation completes quickly while events are processed reliably in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Domain events are a powerful pattern for decoupling business logic, and you don't need a heavyweight framework to use them effectively. The implementation we've built here provides a solid foundation that you can extend as your needs grow.&lt;/p&gt;

&lt;p&gt;The beauty of rolling your own solution is that you understand every piece, making debugging and customization straightforward. This pattern fits excellently in &lt;a href="https://www.milanjovanovic.tech/ddd-refactoring" rel="noopener noreferrer"&gt;&lt;strong&gt;Domain-Driven Design&lt;/strong&gt;&lt;/a&gt; and&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Clean Architecture&lt;/strong&gt;&lt;/a&gt; systems where decoupling business logic is crucial.&lt;/p&gt;

&lt;p&gt;For systems requiring bulletproof reliability or cross-service communication, invest in proper message infrastructure. But for many applications, this simple approach hits the sweet spot between coupling and complexity.&lt;/p&gt;

&lt;p&gt;The key insight is understanding your trade-offs upfront rather than discovering them in production. Start simple, measure what matters, and evolve based on real requirements.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>domainevents</category>
      <category>messaging</category>
      <category>cqrs</category>
      <category>ddd</category>
    </item>
    <item>
      <title>CQRS Pattern the Way It Should've Been From the Start</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 17 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/cqrs-pattern-the-way-it-shouldve-been-from-the-start-g12</link>
      <guid>https://dev.to/milanjovanovictech/cqrs-pattern-the-way-it-shouldve-been-from-the-start-g12</guid>
      <description>&lt;p&gt;MediatR is going commercial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/" rel="noopener noreferrer"&gt;Jimmy Bogard recently announced&lt;/a&gt;that MediatR will adopt a commercial license model for companies above a certain size.&lt;/p&gt;

&lt;p&gt;For many teams, this is a trigger to re-evaluate their usage and possibly look for alternatives.&lt;/p&gt;

&lt;p&gt;And it's not a bad time to do so. MediatR became almost synonymous with CQRS in .NET, despite the fact that &lt;a href="https://www.milanjovanovic.tech/blog/stop-conflating-cqrs-and-mediatr" rel="noopener noreferrer"&gt;&lt;strong&gt;CQRS and MediatR are not the same thing&lt;/strong&gt;&lt;/a&gt;. Most projects use it as a thin dispatching layer for commands and queries — a use case that can be covered with a few straightforward abstractions.&lt;/p&gt;

&lt;p&gt;By removing MediatR, you gain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full control over your CQRS infrastructure&lt;/li&gt;
&lt;li&gt;Predictable, explicit handler dispatching&lt;/li&gt;
&lt;li&gt;Simpler debugging and onboarding&lt;/li&gt;
&lt;li&gt;Cleaner DI setup and better testability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I'll walk you through building a minimal CQRS setup with just a few interfaces and support for decorators. No hidden DI magic. Just clean, predictable code.&lt;/p&gt;

&lt;p&gt;We'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Defining &lt;code&gt;ICommand&lt;/code&gt;, &lt;code&gt;IQuery&lt;/code&gt;, and handler contracts&lt;/li&gt;
&lt;li&gt;Adding support for decorators (logging, validation, etc.)&lt;/li&gt;
&lt;li&gt;Registering everything with DI&lt;/li&gt;
&lt;li&gt;A full working example in a real-world scenario&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands, Queries, and Handlers
&lt;/h2&gt;

&lt;p&gt;Let's start by defining the basic contracts for commands and queries.&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="c1"&gt;// ICommand.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommand&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;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// IQuery.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These interfaces exist purely as markers. They allow us to structure application logic around intention — write operations go through &lt;code&gt;ICommand&lt;/code&gt;, read operations through &lt;code&gt;IQuery&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The handler interfaces follow the same model:&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="c1"&gt;// ICommandHandler.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;
&lt;span class="p"&gt;{&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;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="n"&gt;command&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;cancellationToken&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;interface&lt;/span&gt; &lt;span class="nc"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&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;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="n"&gt;command&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// IQueryHandler.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TQuery&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&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;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TQuery&lt;/span&gt; &lt;span class="n"&gt;query&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;cancellationToken&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;These are nearly identical to MediatR's &lt;code&gt;IRequest&lt;/code&gt; and &lt;code&gt;IRequestHandler&lt;/code&gt; APIs, making migration trivial if you're moving off of MediatR.&lt;/p&gt;

&lt;p&gt;You'll notice we're using a &lt;code&gt;Result&lt;/code&gt; wrapper for all return types. This is optional, but it promotes explicit success/failure handling and encourages consistency across the application boundary. You can learn more about it in my &lt;a href="https://www.milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern" rel="noopener noreferrer"&gt;&lt;strong&gt;previous article&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These interfaces form a lightweight CQRS infrastructure, focused purely on intent and separation of concerns. No mediator, no runtime indirection — just clear contracts for handling reads and writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Example: Command Handler
&lt;/h2&gt;

&lt;p&gt;To see these abstractions in action, let's implement a command that marks a todo item as completed.&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="c1"&gt;// CompleteTodoCommand.cs&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;CompleteTodoCommand&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;TodoItemId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// CompleteTodoCommandHandler.cs&lt;/span&gt;
&lt;span class="k"&gt;internal&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;CompleteTodoCommandHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IApplicationDbContext&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;IDateTimeProvider&lt;/span&gt; &lt;span class="n"&gt;dateTimeProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IUserContext&lt;/span&gt; &lt;span class="n"&gt;userContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CompleteTodoCommand&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CompleteTodoCommand&lt;/span&gt; &lt;span class="n"&gt;command&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;todoItem&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TodoItems&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TodoItemId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;userContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserId&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todoItem&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;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TodoItemErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TodoItemId&lt;/span&gt;&lt;span class="p"&gt;));&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;todoItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt;&lt;span class="p"&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;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TodoItemErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AlreadyCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TodoItemId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;todoItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;todoItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dateTimeProvider&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;todoItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Raise&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;TodoItemCompletedDomainEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;todoItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&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;A few important things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The command is an immutable value object (just data, no behavior).&lt;/li&gt;
&lt;li&gt;The handler encapsulates all business logic: validation, state change, raising domain events, and persistence.&lt;/li&gt;
&lt;li&gt;There's no mediator, no &lt;code&gt;ISender&lt;/code&gt;, no hidden dispatching. The handler is invoked directly via our custom abstractions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes intent explicit, avoids magic, and keeps the dependencies minimal.&lt;/p&gt;

&lt;p&gt;We'll look at how to add decorators next, so we can introduce things like logging, validation, or transactions without modifying the handler itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decorators
&lt;/h2&gt;

&lt;p&gt;To support cross-cutting concerns like logging, validation, and transactions, we apply the &lt;strong&gt;decorator pattern&lt;/strong&gt; around our handlers. Technically, this is closer to the &lt;strong&gt;proxy pattern&lt;/strong&gt; , since we're injecting behavior before/after delegating to the real handler. But in the context of cross-cutting concerns, most people refer to this as a decorator — which is fine for our purposes.&lt;/p&gt;

&lt;p&gt;Let's look at two examples: one for logging, one for validation.&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;Serilog.Context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;internal&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;LoggingCommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;innerHandler&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;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;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;:&lt;/span&gt; &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCommand&lt;/span&gt; &lt;span class="n"&gt;command&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;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&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;commandName&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;TCommand&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Name&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 command {Command}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&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;innerHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&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;"Completed command {Command}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PushProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&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;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Completed command {Command} with error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commandName&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;return&lt;/span&gt; &lt;span class="n"&gt;result&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;This class wraps any &lt;code&gt;ICommandHandler&amp;lt;TCommand, TResponse&amp;gt;&lt;/code&gt;, injecting the decorated handler as &lt;code&gt;innerHandler&lt;/code&gt;. It adds structured logging around the command execution without touching the core business logic.&lt;/p&gt;

&lt;p&gt;Now a &lt;a href="https://www.milanjovanovic.tech/blog/cqrs-validation-with-mediatr-pipeline-and-fluentvalidation" rel="noopener noreferrer"&gt;&lt;strong&gt;validation example with FluentValidation&lt;/strong&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using FluentValidation;
using FluentValidation.Results;

internal sealed class ValidationCommandHandler&amp;lt;TCommand, TResponse&amp;gt;(
    ICommandHandler&amp;lt;TCommand, TResponse&amp;gt; innerHandler,
    IEnumerable&amp;lt;IValidator&amp;lt;TCommand&amp;gt;&amp;gt; validators)
    : ICommandHandler&amp;lt;TCommand, TResponse&amp;gt;
    where TCommand : ICommand&amp;lt;TResponse&amp;gt;
{
    public async Task&amp;lt;Result&amp;lt;TResponse&amp;gt;&amp;gt; Handle(TCommand command, CancellationToken cancellationToken)
    {
        // Validate the command using all registered validators
        ValidationFailure[] validationFailures = await ValidateAsync(command, validators);

        if (validationFailures.Length == 0)
        {
            return await innerHandler.Handle(command, cancellationToken);
        }

        // If validation fails, return a failure result with the errors
        return Result.Failure&amp;lt;TResponse&amp;gt;(CreateValidationError(validationFailures));
    }

    private static async Task&amp;lt;ValidationFailure[]&amp;gt; ValidateAsync&amp;lt;TCommand&amp;gt;(
        TCommand command,
        IEnumerable&amp;lt;IValidator&amp;lt;TCommand&amp;gt;&amp;gt; validators)
    {
        if (!validators.Any())
        {
            return [];
        }

        var context = new ValidationContext&amp;lt;TCommand&amp;gt;(command);

        ValidationResult[] validationResults = await Task.WhenAll(
            validators.Select(validator =&amp;gt; validator.ValidateAsync(context)));

        ValidationFailure[] validationFailures = validationResults
            .Where(validationResult =&amp;gt; !validationResult.IsValid)
            .SelectMany(validationResult =&amp;gt; validationResult.Errors)
            .ToArray();

        return validationFailures;
    }

    private static ValidationError CreateValidationError(ValidationFailure[] validationFailures) =&amp;gt;
        new(validationFailures.Select(f =&amp;gt; Error.Problem(f.ErrorCode, f.ErrorMessage)).ToArray());
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each decorator handles a single concern and can be layered transparently around the core handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Since we're working with generic interfaces (&lt;code&gt;ICommandHandler&amp;lt;,&amp;gt;&lt;/code&gt;, &lt;code&gt;IQueryHandler&amp;lt;,&amp;gt;&lt;/code&gt;), each decorator must explicitly target the same generic contract. That means you'll need separate decorator classes for each handler abstraction you're using (e.g. command with result, command without result, query with result).&lt;/p&gt;

&lt;p&gt;In the next section, we'll wire this up using &lt;a href="https://github.com/khellang/Scrutor" rel="noopener noreferrer"&gt;Scrutor&lt;/a&gt;. It's a simple assembly scanning library that helps us register and decorate handlers cleanly. Yes, it uses reflection, but only during startup — and it's fully transparent and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  DI Setup
&lt;/h2&gt;

&lt;p&gt;With our handlers and decorators in place, we can register everything using Scrutor.&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;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromAssembliesOf&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;DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssignableTo&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;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;gt;)),&lt;/span&gt; &lt;span class="n"&gt;publicOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssignableTo&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;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;)),&lt;/span&gt; &lt;span class="n"&gt;publicOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssignableTo&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;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;gt;)),&lt;/span&gt; &lt;span class="n"&gt;publicOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This scans the application assembly and registers all command and query handlers (including internal types) as their respective interfaces.&lt;/p&gt;

&lt;p&gt;Next, we apply decorators for validation and logging:&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;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Decorate&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;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;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;ValidationDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;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;Decorate&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;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;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;ValidationDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandBaseHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;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;Decorate&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;IQueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;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;LoggingDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;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;Decorate&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;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;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;LoggingDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;,&amp;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;Decorate&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;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;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;LoggingDecorator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandBaseHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;Decorate&lt;/code&gt; call wraps the previous registration. &lt;strong&gt;Order matters&lt;/strong&gt; , but it might not be intuitive at first glance.&lt;/p&gt;

&lt;p&gt;The last decorator applied will be the outermost one at runtime. So in this example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;base handler&lt;/strong&gt; is first decorated by &lt;strong&gt;validation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;That composite is then decorated again by &lt;strong&gt;logging&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which means the &lt;strong&gt;logging decorator runs first&lt;/strong&gt; , followed by &lt;strong&gt;validation&lt;/strong&gt; , and then the core handler.&lt;/p&gt;

&lt;p&gt;This order allows logging to capture the full command lifecycle, including any early exits from validation failures.&lt;/p&gt;

&lt;p&gt;With this setup, you now have a fully functional and extensible CQRS pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom handler interfaces&lt;/li&gt;
&lt;li&gt;Clean decorator chain&lt;/li&gt;
&lt;li&gt;Assembly-scanned DI setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Usage from Minimal API
&lt;/h2&gt;

&lt;p&gt;Once everything is wired up, using a command handler from a Minimal API endpoint is straightforward:&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;internal&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;Complete&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEndpoint&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;void&lt;/span&gt; &lt;span class="nf"&gt;MapEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&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;MapPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"todos/{id:guid}/complete"&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;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ICommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CompleteTodoCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&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;cancellationToken&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;command&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;CompleteTodoCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="n"&gt;result&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;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Problem&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;WithTags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Todos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthorization&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;We're injecting the appropriate &lt;code&gt;ICommandHandler&amp;lt;CompleteTodoCommand&amp;gt;&lt;/code&gt; directly into the endpoint. No need for &lt;code&gt;ISender&lt;/code&gt;, no mediator layer, no runtime lookup.&lt;/p&gt;

&lt;p&gt;This keeps the endpoint clean and focused on its primary responsibility: handling HTTP requests.&lt;/p&gt;

&lt;p&gt;Everything is resolved explicitly by the container. This makes the code easier to test, reason about, and trace while maintaining all the benefits of CQRS and separation of concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;CQRS doesn't require a complex framework.&lt;/p&gt;

&lt;p&gt;With a few small interfaces, some decorator classes, and a clean DI setup, you can build a simple and flexible pipeline for handling commands and queries. It's easy to understand, easy to test, and easy to extend.&lt;/p&gt;

&lt;p&gt;If you want to see this pattern applied in a complete solution, my &lt;a href="https://www.milanjovanovic.tech/templates/clean-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;free Clean Architecture template&lt;/strong&gt;&lt;/a&gt; includes everything covered in this article (fully wired up).&lt;/p&gt;

&lt;p&gt;Use it as a reference or as a starting point for your next project.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>cqrs</category>
      <category>architecture</category>
      <category>mediatr</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>From Anemic Models to Behavior-Driven Models: A Practical DDD Refactor in C#</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 10 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/from-anemic-models-to-behavior-driven-models-a-practical-ddd-refactor-in-c-f35</link>
      <guid>https://dev.to/milanjovanovictech/from-anemic-models-to-behavior-driven-models-a-practical-ddd-refactor-in-c-f35</guid>
      <description>&lt;p&gt;If you've ever worked with a legacy C# codebase, you know the pain of an anemic domain model. You have probably opened an &lt;code&gt;OrderService&lt;/code&gt; (&lt;em&gt;all similarities to production code are merely a coincidence&lt;/em&gt;) and thought _"this file does everything."_Pricing logic, discount rules, stock checks, database writes — &lt;strong&gt;all jam-packed into one class&lt;/strong&gt;. It works — until it doesn't. New features turn into &lt;strong&gt;regression roulette&lt;/strong&gt; , and test coverage plummets because the domain is buried under infrastructure.&lt;/p&gt;

&lt;p&gt;This is the classic symptom of an anemic domain model, where entities are nothing but data holders, and all logic lives elsewhere. It makes the system harder to reason about, and every change becomes a guessing game. But what if we could push behavior back into the domain, one rule at a time?&lt;/p&gt;

&lt;p&gt;In this article, we'll:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inspect&lt;/strong&gt; a typical anemic implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify&lt;/strong&gt; hidden business rules that make it brittle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactor&lt;/strong&gt; toward a behavior-rich aggregate one refactor at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Highlight&lt;/strong&gt; the concrete payoffs so you can justify the change to teammates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything fits in a 6-minute read, but the pattern scales to any legacy system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting Point: God-like Service Class
&lt;/h2&gt;

&lt;p&gt;Below is an (unfortunately common) &lt;code&gt;OrderService&lt;/code&gt;. Besides calculating totals it also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;applies a &lt;strong&gt;5 % VIP discount&lt;/strong&gt; ,&lt;/li&gt;
&lt;li&gt;throws if any product is &lt;strong&gt;out of stock&lt;/strong&gt; , and&lt;/li&gt;
&lt;li&gt;rejects orders that would &lt;strong&gt;exceed the customer's credit limit&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// OrderService.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PlaceOrder&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="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItemDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&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;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer not found"&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="k"&gt;new&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;customerId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;foreach&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;dto&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&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;inventory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_inventoryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&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;inventory&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&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;"Item out of stock"&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;price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_pricingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&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;lineTotal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lineTotal&lt;/span&gt; &lt;span class="p"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;0.95m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5% discount for VIPs&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;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OrderItem&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ProductId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;UnitPrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;LineTotal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lineTotal&lt;/span&gt;
        &lt;span class="p"&gt;});&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;Total&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;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LineTotal&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreditUsed&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;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreditLimit&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;"Credit limit exceeded"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&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;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&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;
  
  
  What's Wrong Here?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scattered rules:&lt;/strong&gt; Discount application, stock validation, and credit-limit checks are buried inside the service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tight coupling:&lt;/strong&gt; &lt;code&gt;OrderService&lt;/code&gt; must know about pricing, inventory, and EF Core just to place an order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Painful testing:&lt;/strong&gt; Each unit test needs fakes for DB access, pricing, inventory, and VIP vs. non-VIP flows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Embed these rules &lt;strong&gt;inside the domain&lt;/strong&gt; so the application layer only deals with orchestration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guiding Principles Before We Touch Code
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protect invariants close to the data.&lt;/strong&gt; Stock, discounts, and credit checks belong where the data lives — inside the &lt;code&gt;Order&lt;/code&gt; aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose intent, hide mechanics.&lt;/strong&gt; The application layer should read like a story: &lt;em&gt;"place order"&lt;/em&gt;, not &lt;em&gt;"calculate totals, check credit, write to DB"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactor in slices.&lt;/strong&gt; Each move is safe and compilable; no big-bang rewrites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balance purity with pragmatism.&lt;/strong&gt; Move rules only when the payoff (clarity, safety, testability) beats the extra lines of code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step-by-Step Refactor
&lt;/h2&gt;

&lt;p&gt;The goal here isn't to chase purity or academic DDD. It's to incrementally improve cohesion and make room for the domain to express itself.&lt;/p&gt;

&lt;p&gt;At every step, we ask: Is this behavior something the domain should own? If yes, we pull it inward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embed Creation &amp;amp; Validation Logic
&lt;/h3&gt;

&lt;p&gt;The first move is to make the aggregate responsible for building itself. A static &lt;code&gt;Create&lt;/code&gt; method gives us a single entry point where all invariants can fail fast.&lt;/p&gt;

&lt;p&gt;While pushing stock validation into &lt;code&gt;Order&lt;/code&gt; improves testability, it does couple the order flow with inventory availability. In some domains, you'd instead model this as a domain event and validate asynchronously.&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="c1"&gt;// Order.cs (Factory Method)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IPricingService&lt;/span&gt; &lt;span class="n"&gt;pricingService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IInventoryService&lt;/span&gt; &lt;span class="n"&gt;inventoryService&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&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;inventoryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;quantity&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;"Item out of stock"&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;unitPrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pricingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&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="nf"&gt;AddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsVip&lt;/span&gt;&lt;span class="p"&gt;);&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="nf"&gt;EnsureCreditWithinLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&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;order&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;&lt;strong&gt;Why?&lt;/strong&gt; Creation now &lt;strong&gt;fails fast&lt;/strong&gt; if any invariant is broken. The service no longer micromanages stock or discounts.&lt;/p&gt;

&lt;p&gt;Notice how we're now following the "Tell, Don't Ask" principle. Rather than the service checking conditions and then manipulating the Order, we're telling the Order to create itself with the necessary validations built in. This is a fundamental shift toward &lt;strong&gt;encapsulation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡 On Double-Dispatch in Domain Methods&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Passing services into domain methods like &lt;code&gt;Order.Create&lt;/code&gt; might raise a few eyebrows. But in this case, it's an explicit form of double-dispatch that enables us to keep complex logic inside the domain model without bloating the application service. It gives the entity autonomy while still respecting dependency injection principles — the services are passed explicitly, not resolved implicitly. That said, this approach is best used sparingly and only when the operation truly belongs inside the domain object.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guard the Aggregate's Internal State
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Order.cs (excerpt)&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_items&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="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsReadOnly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// C# 12 -&amp;gt; [.._items]&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddItem&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;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&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;unitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isVip&lt;/span&gt;&lt;span class="p"&gt;)&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;quantity&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quantity must be positive"&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;finalPrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isVip&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;unitPrice&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.95m&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&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;OrderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalPrice&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nf"&gt;RecalculateTotal&lt;/span&gt;&lt;span class="p"&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;void&lt;/span&gt; &lt;span class="nf"&gt;EnsureCreditWithinLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreditUsed&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreditLimit&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;"Credit limit exceeded"&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;&lt;strong&gt;Why bother?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encapsulation&lt;/strong&gt; : Consumers can't mutate &lt;code&gt;_items&lt;/code&gt; directly, ensuring invariants hold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-protection&lt;/strong&gt; : The domain model protects its own consistency rather than relying on service-level checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;True OOP&lt;/strong&gt; : Objects now combine data and behavior, as object-oriented programming intended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler services&lt;/strong&gt; : Application services can focus on coordination rather than business rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Shrink the Application Layer to Pure Orchestration
&lt;/h3&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;void&lt;/span&gt; &lt;span class="nf"&gt;PlaceOrder&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="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderLineDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lines&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;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer not found"&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;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&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;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&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;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&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;_pricingService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_inventoryService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&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;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&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 &lt;code&gt;PlaceOrder&lt;/code&gt; method drops from &lt;strong&gt;44 lines&lt;/strong&gt; to &lt;strong&gt;14&lt;/strong&gt; , with &lt;strong&gt;zero business logic&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Gained
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before the refactor&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service owned pricing, stock, discount, and credit checks.&lt;/li&gt;
&lt;li&gt;Unit tests required heavy EF Core and service fakes.&lt;/li&gt;
&lt;li&gt;Adding a new rule meant touching multiple files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After the refactor&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aggregate owns all business rules; service only orchestrates.&lt;/li&gt;
&lt;li&gt;Pure domain tests — no database container required.&lt;/li&gt;
&lt;li&gt;Most changes are isolated to the &lt;code&gt;Order&lt;/code&gt; aggregate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The real value in refactoring anemic models isn't technical — it's strategic.&lt;/p&gt;

&lt;p&gt;By moving business logic closer to the data, you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce the blast radius of changes&lt;/li&gt;
&lt;li&gt;Make business rules explicit and testable&lt;/li&gt;
&lt;li&gt;Open the door for tactical patterns like validation, events, and invariants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you don't need a big rewrite. Start with one rule. Refactor it. Then the next.&lt;/p&gt;

&lt;p&gt;That's how legacy systems evolve into maintainable architectures.&lt;/p&gt;

&lt;p&gt;If you enjoyed this breakdown and want a hands-on, real-world guide to untangling messy services, check out my course &lt;a href="https://www.milanjovanovic.tech/ddd-refactoring" rel="noopener noreferrer"&gt;&lt;strong&gt;Domain-Driven Design Refactoring&lt;/strong&gt;&lt;/a&gt;. It's packed with before-and-after examples like this one.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 4,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ddd</category>
      <category>anemic</category>
      <category>refactoring</category>
      <category>domainmodel</category>
    </item>
    <item>
      <title>Event-Driven Architecture in .NET with RabbitMQ</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 03 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/event-driven-architecture-in-net-with-rabbitmq-538m</link>
      <guid>https://dev.to/milanjovanovictech/event-driven-architecture-in-net-with-rabbitmq-538m</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Event-driven_architecture" rel="noopener noreferrer"&gt;Event-driven architecture&lt;/a&gt; (EDA) can make applications more flexible and reliable. Instead of one part of the system calling another directly, we let events flow through a message broker. In this quick guide, I'll show you how to set up a simple event-driven system in .NET using &lt;a href="https://www.rabbitmq.com/" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We'll build a small example with a producer that sends events and a consumer that receives them. For testing, I'll run RabbitMQ in a Docker container (with the Management UI enabled so we can see what's happening). We'll use the official &lt;a href="https://www.nuget.org/packages/rabbitmq.client/" rel="noopener noreferrer"&gt;RabbitMQ.Client&lt;/a&gt; NuGet package in a .NET console app.&lt;/p&gt;

&lt;p&gt;Note: If you don't have RabbitMQ installed, you can run it quickly with Docker. For example:&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;docker&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;rm&lt;/span&gt; &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;rabbitmq&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="m"&gt;5672&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;p&lt;/span&gt; &lt;span class="m"&gt;15672&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;15672&lt;/span&gt; &lt;span class="n"&gt;rabbitmq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;management&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts a RabbitMQ broker on localhost (AMQP port &lt;code&gt;5672&lt;/code&gt;) and a management website at &lt;code&gt;http://localhost:15672&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  RabbitMQ Basics
&lt;/h2&gt;

&lt;p&gt;Before coding, let's cover the basic components in RabbitMQ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Producer&lt;/strong&gt; : an application that sends messages (events) to RabbitMQ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue&lt;/strong&gt; : a mailbox inside RabbitMQ that stores messages. Consumers read from queues. Many producers can send to the same queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer&lt;/strong&gt; : an application that receives messages from a queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange&lt;/strong&gt; : a routing mechanism that receives messages from producers and directs them to queues. Producers actually send to an exchange instead of directly to a queue. This decouples producers from specific queues - the exchange can decide where messages go, based on rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In RabbitMQ, you can have multiple producers and multiple consumers. Producers never send directly to a queue by name; instead, they send to an exchange. The exchange decides which queues (if any) should get each message based on routing rules.&lt;/p&gt;

&lt;p&gt;For now, we'll use a simple setup where the exchange will deliver all messages to one queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Producer - Sending Events
&lt;/h2&gt;

&lt;p&gt;Let's start with the producer. In our .NET console app, we'll use RabbitMQ.Client to connect to the RabbitMQ broker and send a message.&lt;/p&gt;

&lt;p&gt;For instance, an &lt;code&gt;OrderPlaced&lt;/code&gt; event could trigger downstream services - inventory, email notifications, etc. - without the ordering system needing to call them directly.&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;factory&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;ConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;()&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="s"&gt;"localhost"&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;var&lt;/span&gt; &lt;span class="n"&gt;connection&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;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateConnectionAsync&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;var&lt;/span&gt; &lt;span class="n"&gt;channel&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;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateChannelAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Ensure the queue exists (create it if not already there)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&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;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// save to disk so the queue isn’t lost on broker restart&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// can be used by other connections&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// don’t delete when the last consumer disconnects&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create a message&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderPlaced&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OrderPlaced&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;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;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;99.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&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="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderPlaced&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;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Publish the message&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicPublishAsync&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default exchange&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;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// fail if the message can’t be routed&lt;/span&gt;
    &lt;span class="n"&gt;basicProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BasicProperties&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Persistent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// message will be saved to disk&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&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;$"Sent: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code connects to RabbitMQ on &lt;code&gt;localhost&lt;/code&gt;, declares a queue named &lt;code&gt;orders&lt;/code&gt; (creates it if it doesn't exist already), and publishes an &lt;code&gt;OrderPlaced&lt;/code&gt; message to that queue. We use an empty string for the exchange parameter, which tells RabbitMQ to use the default exchange. The default exchange routes the message directly to the &lt;code&gt;orders&lt;/code&gt; queue.&lt;/p&gt;

&lt;p&gt;What's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We declare a &lt;strong&gt;durable queue&lt;/strong&gt; , so it survives RabbitMQ restarts&lt;/li&gt;
&lt;li&gt;We mark the message as &lt;strong&gt;persistent&lt;/strong&gt; , which tells RabbitMQ to write it to disk&lt;/li&gt;
&lt;li&gt;We serialize an object into JSON and send it as a UTF-8 encoded byte array&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's look at the consumer side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumer - Receiving Events
&lt;/h2&gt;

&lt;p&gt;Next, let's set up a consumer to receive messages from the queue. The consumer will also connect to RabbitMQ and subscribe to the same queue.&lt;/p&gt;

&lt;p&gt;To test this out, start the consumer application first (it will wait for messages), then run the producer application to send an event.&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;factory&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;ConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;()&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="s"&gt;"localhost"&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;var&lt;/span&gt; &lt;span class="n"&gt;connection&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;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateConnectionAsync&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;var&lt;/span&gt; &lt;span class="n"&gt;channel&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;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateChannelAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Declare (or check) the queue to consume from&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&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;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// must match the producer's queue settings&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// can be used by other connections&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// don’t delete when the last consumer disconnects&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Define a consumer and start listening&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;consumer&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;AsyncEventingBasicConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReceivedAsync&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventArgs&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="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eventArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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;orderPlaced&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;OrderPlaced&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;message&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;$"Received: OrderPlaced - &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;orderPlaced&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Acknowledge the message&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;AsyncEventingBasicConsumer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicAckAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicConsumeAsync&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;autoAck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer&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;"Waiting for messages..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer code declares the same &lt;code&gt;orders&lt;/code&gt; queue and sets up an event handler for incoming messages. We call &lt;code&gt;BasicConsumeAsync&lt;/code&gt; to start listening on the queue. RabbitMQ will push any new messages to our consumer's event handler. Whenever a message arrives, the &lt;code&gt;consumer.ReceivedAsync&lt;/code&gt; event fires, and we print out the message.&lt;/p&gt;

&lt;p&gt;What's important here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;autoAck: false&lt;/code&gt; ensures we only acknowledge messages we actually process&lt;/li&gt;
&lt;li&gt;If processing fails, we could use &lt;code&gt;BasicNack&lt;/code&gt; to requeue or route to a dead-letter queue&lt;/li&gt;
&lt;li&gt;Deserializing into a strongly typed object makes it easy to reason about the event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far we've had one consumer. But what if we run multiple consumers on the same queue?&lt;/p&gt;

&lt;h2&gt;
  
  
  Competing Consumers - Scaling Out
&lt;/h2&gt;

&lt;p&gt;What if you have multiple consumers for the same queue? RabbitMQ allows &lt;strong&gt;competing consumers&lt;/strong&gt; on a queue.&lt;/p&gt;

&lt;p&gt;If two or more consumers listen on the same queue, each message from that queue will be delivered to &lt;strong&gt;only one&lt;/strong&gt; of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RabbitMQ will distribute messages among the consumers (roughly in round-robin order)&lt;/li&gt;
&lt;li&gt;This is great for scaling: you can run multiple instances of a worker to process messages in parallel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, consumers &lt;em&gt;compete&lt;/em&gt; for messages on that queue. This pattern helps spread the workload, but note that each individual message is still processed by a single consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fanout Exchange: Broadcast to Multiple Consumers
&lt;/h2&gt;

&lt;p&gt;Competing consumers share the work by dividing messages, but sometimes you want every service to get the event. That's where a &lt;strong&gt;fanout exchange&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;In RabbitMQ, a fanout exchange is used for broadcasting events to multiple consumers. Instead of all consumers sharing one queue, each consumer has its own queue. When the producer sends a message to a fanout exchange, the exchange copies and routes the message to all bound queues. This way, every consumer receives a copy via its own queue.&lt;/p&gt;

&lt;p&gt;To set this up in code, we declare a fanout exchange and bind queues to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer&lt;/strong&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="c1"&gt;// Producer setup for fanout&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExchangeDeclareAsync&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// durable exchange&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// don’t delete when the last consumer disconnects&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExchangeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fanout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Publish a message to the fanout exchange (routingKey is ignored for fanout)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderPlaced&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OrderPlaced&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;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;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;99.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&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="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderPlaced&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;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicPublishAsync&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"&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mandatory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;basicProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BasicProperties&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Persistent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Consumer&lt;/strong&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="c1"&gt;// Consumer setup for fanout&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExchangeDeclareAsync&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExchangeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fanout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create a queue for this consumer and bind it&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"orders-consumer-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueBindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-consumer-1"&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;routingKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Then consume messages from queueName as usual...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the producer, we call &lt;code&gt;ExchangeDeclareAsync&lt;/code&gt; to make sure an &lt;code&gt;orders&lt;/code&gt; exchange exists (of type fanout). We then &lt;code&gt;BasicPublishAsync&lt;/code&gt; to that exchange. For a fanout exchange, the &lt;code&gt;routingKey&lt;/code&gt; can be an empty string because it's ignored (fanout sends to all queues regardless of any routing key). On the consumer side, we declare the same exchange and then create a new &lt;code&gt;orders-consumer-1&lt;/code&gt; queue. We bind that queue to the &lt;code&gt;orders&lt;/code&gt; exchange. Now any message sent to the exchange will be delivered to this queue, and we can consume it.&lt;/p&gt;

&lt;p&gt;If you run multiple consumer programs (each with its own queue bound to &lt;code&gt;orders&lt;/code&gt; exchange), each one will get every message (unlike the competing consumers scenario). You can also peek into RabbitMQ's Management UI to see the exchange and queues in action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvc27mwly7h7ddx0jz4ro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvc27mwly7h7ddx0jz4ro.png" alt="Image description" width="800" height="869"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;You can expand this basic setup with more advanced RabbitMQ features. For example, you might use a &lt;strong&gt;direct exchange&lt;/strong&gt; or &lt;strong&gt;topic exchange&lt;/strong&gt; to route events to specific services, set up acknowledgment and retry policies for robustness, or implement &lt;strong&gt;dead-letter queues&lt;/strong&gt; for error handling. The core idea throughout is the same: decouple senders and receivers with an event broker, making your system more flexible and resilient.&lt;/p&gt;

&lt;p&gt;If you want to explore event-driven architecture further, including patterns like the ones we touched on (and beyond), check out my &lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Modular Monolith Architecture&lt;/strong&gt;&lt;/a&gt; course. It covers these concepts in depth with practical examples, so you can apply EDA in real-world projects.&lt;/p&gt;

&lt;p&gt;Good luck out there, and see you next week.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 4,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>messaging</category>
      <category>rabbitmq</category>
      <category>distributedsystem</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Refactoring Overgrown Bounded Contexts in Modular Monoliths</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 26 Apr 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/refactoring-overgrown-bounded-contexts-in-modular-monoliths-1a2f</link>
      <guid>https://dev.to/milanjovanovictech/refactoring-overgrown-bounded-contexts-in-modular-monoliths-1a2f</guid>
      <description>&lt;p&gt;When you're building a &lt;a href="https://www.milanjovanovic.tech/blog/what-is-a-modular-monolith" rel="noopener noreferrer"&gt;&lt;strong&gt;modular monolith&lt;/strong&gt;&lt;/a&gt;, it's easy to let bounded contexts grow too large over time. What started as a clean domain boundary slowly turns into a dumping ground for unrelated logic. Before you know it, you have a massive context responsible for users, payments, notifications, and reporting - all tangled together.&lt;/p&gt;

&lt;p&gt;This article is about tackling that mess. We'll walk through how to identify an overgrown bounded context, and refactor it step-by-step into smaller, well-defined contexts. You'll see practical techniques in action, with real .NET code and without theoretical fluff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying an Overgrown Context
&lt;/h2&gt;

&lt;p&gt;You know you have a problem when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're afraid to touch code because everything is interconnected&lt;/li&gt;
&lt;li&gt;The same entity is used for 4 unrelated use cases&lt;/li&gt;
&lt;li&gt;You see classes with 1000+ lines or services that do too much&lt;/li&gt;
&lt;li&gt;Business logic from different subdomains bleeds into each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a classic example.&lt;/p&gt;

&lt;p&gt;We start with a &lt;code&gt;BillingContext&lt;/code&gt; that now handles everything from notifications to reporting:&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;class&lt;/span&gt; &lt;span class="nc"&gt;BillingService&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;void&lt;/span&gt; &lt;span class="nf"&gt;ChargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;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;void&lt;/span&gt; &lt;span class="nf"&gt;SendInvoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;invoiceId&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;NotifyCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;string&lt;/span&gt; &lt;span class="n"&gt;message&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;GenerateMonthlyReport&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;DeactivateUserAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&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="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;This service has no clear boundaries. It mixes &lt;strong&gt;Billing&lt;/strong&gt; , &lt;strong&gt;Notifications&lt;/strong&gt; , &lt;strong&gt;Reporting&lt;/strong&gt; , and &lt;strong&gt;User Management&lt;/strong&gt; into a single, bloated class. Changing one feature could easily break another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Identify Logical Subdomains
&lt;/h2&gt;

&lt;p&gt;We start by breaking this apart logically. Think like a product owner.&lt;/p&gt;

&lt;p&gt;Just ask: "What domains are we really working with?"&lt;/p&gt;

&lt;p&gt;Group the methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Billing&lt;/strong&gt; : &lt;code&gt;ChargeCustomer&lt;/code&gt;, &lt;code&gt;SendInvoice&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; : &lt;code&gt;NotifyCustomer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting&lt;/strong&gt; : &lt;code&gt;GenerateMonthlyReport&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Management&lt;/strong&gt; : &lt;code&gt;DeactivateUserAccount&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code within a &lt;a href="https://www.milanjovanovic.tech/blog/monolith-to-microservices-how-a-modular-monolith-helps" rel="noopener noreferrer"&gt;&lt;strong&gt;bounded context&lt;/strong&gt;&lt;/a&gt; should model a coherent domain. When multiple domains are jammed into the same context, your architecture becomes misleading.&lt;/p&gt;

&lt;p&gt;You can validate these groupings by checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which parts of the system change together?&lt;/li&gt;
&lt;li&gt;Do teams use different vocabulary for each area?&lt;/li&gt;
&lt;li&gt;Would you give each domain to a different team?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes, it's a sign you're dealing with distinct contexts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Extract One Context at a Time
&lt;/h2&gt;

&lt;p&gt;Don't try to do it all at once. Start with something low-risk.&lt;/p&gt;

&lt;p&gt;Let's begin by extracting &lt;strong&gt;Notifications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why &lt;strong&gt;Notifications&lt;/strong&gt;? Because it's a pure side-effect. It doesn't impact business state, so it's easier to decouple safely.&lt;/p&gt;

&lt;p&gt;Create a new module and move the logic there:&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="c1"&gt;// New module: Notifications&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;NotificationService&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;void&lt;/span&gt; &lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;string&lt;/span&gt; &lt;span class="n"&gt;message&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="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;Then simplify the original &lt;code&gt;BillingService&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BillingService&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;NotificationService&lt;/span&gt; &lt;span class="n"&gt;_notificationService&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;BillingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationService&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_notificationService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notificationService&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;void&lt;/span&gt; &lt;span class="nf"&gt;ChargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Charge logic...&lt;/span&gt;
        &lt;span class="n"&gt;_notificationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&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="s"&gt;$"You were charged $&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="s"&gt;"&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;This works. But now &lt;strong&gt;Billing&lt;/strong&gt; &lt;em&gt;depends on&lt;/em&gt; &lt;strong&gt;Notifications&lt;/strong&gt;. That's a coupling we want to avoid long-term.&lt;/p&gt;

&lt;p&gt;Why? Because a failure in &lt;strong&gt;Notifications&lt;/strong&gt; could block a billing operation. It also means &lt;strong&gt;Billing&lt;/strong&gt; can't evolve independently.&lt;/p&gt;

&lt;p&gt;Let's decouple with &lt;a href="https://www.milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems" rel="noopener noreferrer"&gt;&lt;strong&gt;domain events&lt;/strong&gt;&lt;/a&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomerChargedEvent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&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="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="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&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="c1"&gt;// Module: Billing&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;BillingService&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;IDomainEventDispatcher&lt;/span&gt; &lt;span class="n"&gt;_dispatcher&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;BillingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDomainEventDispatcher&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_dispatcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dispatcher&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;void&lt;/span&gt; &lt;span class="nf"&gt;ChargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&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;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Charge logic...&lt;/span&gt;
        &lt;span class="n"&gt;_dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerChargedEvent&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;customerId&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;amount&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="c1"&gt;// Module: Notifications&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;CustomerChargedEventnHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDomainEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerChargedEvent&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="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CustomerChargedEvent&lt;/span&gt; &lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Send notification&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;Now &lt;strong&gt;Billing&lt;/strong&gt; doesn't even &lt;em&gt;know&lt;/em&gt; about &lt;strong&gt;Notifications&lt;/strong&gt;. That's real modularity. You can replace, remove, or enhance the &lt;strong&gt;Notifications&lt;/strong&gt; module without touching &lt;strong&gt;Billing&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Migrate Data (If Needed)
&lt;/h2&gt;

&lt;p&gt;Most monoliths start with a single database. That's fine. But real modularity comes when each module controls its own &lt;a href="https://www.milanjovanovic.tech/blog/modular-monolith-data-isolation" rel="noopener noreferrer"&gt;&lt;strong&gt;schema&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because the database structure reflects ownership. If everything touches the same tables, it's hard to enforce boundaries.&lt;/p&gt;

&lt;p&gt;You don't have to do it all at once. Start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a &lt;a href="https://www.milanjovanovic.tech/blog/using-multiple-ef-core-dbcontext-in-single-application" rel="noopener noreferrer"&gt;&lt;strong&gt;separate &lt;code&gt;DbContext&lt;/code&gt; per module&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gradually migrate the tables to their own schemas&lt;/li&gt;
&lt;li&gt;Read-only projections or database views for cross-context reads
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Module: Billing&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;BillingDbContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&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;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Invoices&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="c1"&gt;// Module: Notifications&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;NotificationsDbContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&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;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NotificationLog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Logs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;This separation enables independent schema evolution. It also makes &lt;a href="https://www.milanjovanovic.tech/blog/testing-modular-monoliths-system-integration-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;testing&lt;/strong&gt;&lt;/a&gt; faster and safer.&lt;/p&gt;

&lt;p&gt;When migrating, use a transitional phase where both contexts read from the same underlying data. Only switch write paths when confidence is high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Repeat for Other Areas
&lt;/h2&gt;

&lt;p&gt;Apply the same playbook. Target a clean split per subdomain.&lt;/p&gt;

&lt;p&gt;Next up: &lt;strong&gt;Reporting&lt;/strong&gt; and &lt;strong&gt;User Management&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before:&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;billingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateMonthlyReport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;billingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeactivateUserAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&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;reportingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateMonthlyReport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeactivateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via events:&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;_dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispatch&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;MonthEndedEvent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;_dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispatch&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;UserInactiveEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal here isn't just technical cleanliness - it's clarity. Anyone looking at your solution should know what each module is responsible for.&lt;/p&gt;

&lt;p&gt;And remember: boundaries should be enforced by code, not just by folder structure. Different projects, separate EF models, and &lt;a href="https://www.milanjovanovic.tech/blog/internal-vs-public-apis-in-modular-monoliths" rel="noopener noreferrer"&gt;&lt;strong&gt;explicit interfaces&lt;/strong&gt;&lt;/a&gt; help enforce the split.&lt;a href="https://www.milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests" rel="noopener noreferrer"&gt;&lt;strong&gt;Architecture tests&lt;/strong&gt;&lt;/a&gt; can also help ensure that modules don't break their boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Once you've finished the refactor, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smaller services&lt;/strong&gt; focused on one job&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoupled modules&lt;/strong&gt; that evolve independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better tests&lt;/strong&gt; and easier debugging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded contexts&lt;/strong&gt; that actually match the domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more than structure, it's design that supports change. You get loose coupling, testability, and clearer mental models.&lt;/p&gt;

&lt;p&gt;You don't need microservices to get modularity. You need to treat your monolith like a set of cooperating, isolated parts.&lt;/p&gt;

&lt;p&gt;Start with one module. Ship the change. Repeat.&lt;/p&gt;

&lt;p&gt;Want to go deeper into modular monolith design? My full video course, &lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Modular Monolith Architecture&lt;/strong&gt;&lt;/a&gt;, walks you through building a real-world system from scratch - with clear boundaries, isolated modules, and practical patterns that scale. Join 1,800+ students and start building better systems today.&lt;/p&gt;

&lt;p&gt;That's all for today.&lt;/p&gt;

&lt;p&gt;See you next Saturday.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 4,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>modularmonolith</category>
      <category>architecture</category>
      <category>refactoring</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Understanding Microservices: Core Concepts and Benefits</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 19 Apr 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/understanding-microservices-core-concepts-and-benefits-5bk4</link>
      <guid>https://dev.to/milanjovanovictech/understanding-microservices-core-concepts-and-benefits-5bk4</guid>
      <description>&lt;p&gt;I've been revisiting Sam Newman's excellent book &lt;a href="https://www.oreilly.com/library/view/monolith-to-microservices/9781492047834/" rel="noopener noreferrer"&gt;"Monolith to Microservices"&lt;/a&gt; recently, and it's reminded me just how transformative this architectural approach can be &lt;strong&gt;when applied correctly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As someone who's implemented microservices in various organizations, I wanted to share some valuable insights I've gained through both study and practical experience.&lt;/p&gt;

&lt;p&gt;What exactly are microservices, and why might they be the right architectural choice for your organization?&lt;/p&gt;

&lt;p&gt;Let's dive into the core concepts and benefits of microservices architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Microservices?
&lt;/h2&gt;

&lt;p&gt;Microservices are &lt;strong&gt;independently deployable&lt;/strong&gt; services modeled around a &lt;strong&gt;business domain&lt;/strong&gt;. Business domain is key here, but more on that later. They communicate with each other via networks and offer many options for solving complex architectural problems.&lt;/p&gt;

&lt;p&gt;Think of microservices as small, focused teams rather than a large department. Each team has a specific responsibility, operates somewhat independently, and communicates clearly with other teams when needed. Instead of one massive codebase that handles everything, you have multiple smaller codebases, each focusing on a specific business capability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycwwb76adovagnts7d5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycwwb76adovagnts7d5s.png" alt="Image description" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As Sam Newman defines them in "Monolith to Microservices":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Microservices are independently deployable services modeled around a business domain. They communicate with each other via networks, and as an architecture choice offer many options for solving the problems you may face.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Microservices give you a strategy to design a modular system and decompose it into bounded contexts. However, the problem I often see is that developers use microservices to enforce code boundaries. This is a mistake. We'll fix that in a moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Characteristics of Microservices
&lt;/h2&gt;

&lt;p&gt;Let's explore some of the key characteristics that define microservices:&lt;/p&gt;

&lt;h3&gt;
  
  
  Independent Deployability
&lt;/h3&gt;

&lt;p&gt;You can make changes to a microservice and deploy it to production without having to deploy anything else. This isn't just a theoretical ability - it's a discipline you practice for most of your releases.&lt;/p&gt;

&lt;p&gt;The value here is significant: &lt;strong&gt;smaller deployments&lt;/strong&gt; carry &lt;strong&gt;less risk&lt;/strong&gt; , enable &lt;strong&gt;faster release cycles&lt;/strong&gt; , and allow teams to test their changes in isolation.&lt;/p&gt;

&lt;p&gt;When a critical bug appears in one service, you can fix and deploy just that service rather than orchestrating a full system release. This is especially valuable in large systems where coordinating releases can be a logistical nightmare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Business Domain Focus
&lt;/h3&gt;

&lt;p&gt;Microservices are organized around &lt;a href="https://www.milanjovanovic.tech/blog/screaming-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;business capabilities&lt;/strong&gt;&lt;/a&gt; rather than technical layers. Instead of having separate frontend, backend, and database teams (and the coordination that requires), you might have teams dedicated to "Event Management," "Customer Accounts," or "Attendance."&lt;/p&gt;

&lt;p&gt;This alignment makes it easier to implement business functionality changes since all the related code - from UI to data storage - is grouped together. When a business requirement changes, you can often change just one service rather than coordinating changes across multiple layers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrkp1csf09zj06jo57s6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrkp1csf09zj06jo57s6.png" alt="Image description" width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Ownership
&lt;/h3&gt;

&lt;p&gt;Microservices &lt;strong&gt;encapsulate data storage&lt;/strong&gt; and retrieval, exposing data only via &lt;a href="https://www.milanjovanovic.tech/blog/internal-vs-public-apis-in-modular-monoliths" rel="noopener noreferrer"&gt;&lt;strong&gt;well-defined interfaces&lt;/strong&gt;&lt;/a&gt;. Databases are hidden inside the service boundary rather than shared between services.&lt;/p&gt;

&lt;p&gt;This stands in stark contrast to traditional approaches where multiple applications share a common database, often leading to tight coupling and risky schema changes.&lt;/p&gt;

&lt;p&gt;When a service owns its data exclusively, it can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Evolve its internal data model without breaking other services&lt;/li&gt;
&lt;li&gt;Implement the most appropriate storage technology for its needs&lt;/li&gt;
&lt;li&gt;Provide a stable API for other services to access its data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not uncommon for multiple services to share the same database, but you're giving up some of the benefits of microservices by doing so. In practice, one database per service is the most common approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Communication
&lt;/h3&gt;

&lt;p&gt;Services communicate with each other via networks, making microservices a form of distributed system. This network-based communication could use &lt;a href="https://www.milanjovanovic.tech/pragmatic-rest-apis" rel="noopener noreferrer"&gt;&lt;strong&gt;REST APIs&lt;/strong&gt;&lt;/a&gt;, message queues, gRPC, GraphQL, or other protocols depending on the specific needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuuuzj0ihyl987f0za0ro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuuuzj0ihyl987f0za0ro.png" alt="Image description" width="736" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This explicit communication over networks allows services to be deployed independently and even run on different infrastructure. But it also means dealing with network latency, potential failures, and serialization concerns.&lt;/p&gt;

&lt;p&gt;Teams building microservices need to carefully design their &lt;a href="https://www.milanjovanovic.tech/blog/modular-monolith-communication-patterns" rel="noopener noreferrer"&gt;&lt;strong&gt;inter-service communication patterns&lt;/strong&gt;&lt;/a&gt;to balance performance, reliability, and flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Origins of Microservices
&lt;/h2&gt;

&lt;p&gt;The term "microservices" has an interesting origin story. In 2011, a software consultant named James Lewis became interested in what he called "micro-apps" - small services optimized to be easily replaceable.&lt;/p&gt;

&lt;p&gt;The distinguishing feature was how small in scope these services were. Some could be written or rewritten in just a few days. As discussions evolved, the term "microservices" was adopted since these weren't self-contained applications but rather services working together.&lt;/p&gt;

&lt;p&gt;It's worth noting that while "micro" is in the name, the size of a microservice isn't its defining characteristic. Rather, it's about having services with well-defined boundaries that can be developed, deployed, and scaled independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Benefits of Microservices
&lt;/h2&gt;

&lt;p&gt;What are the benefits of adopting a microservices architecture?&lt;/p&gt;

&lt;p&gt;Why should you consider it for your organization?&lt;/p&gt;

&lt;p&gt;Let's explore some of the key advantages:&lt;/p&gt;

&lt;h3&gt;
  
  
  Flexibility and Adaptability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Microservices give you options&lt;/strong&gt;. They provide flexibility in how you can scale, evolve, and maintain your system over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When business requirements change, you can modify just the affected services rather than risking changes to the entire system.&lt;/li&gt;
&lt;li&gt;New capabilities can be introduced as new services without disrupting existing functionality.&lt;/li&gt;
&lt;li&gt;As your understanding of the domain grows, service boundaries can evolve to better reflect that understanding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ability to evolve incrementally is particularly valuable in rapidly changing business environments where time-to-market is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technology Diversity
&lt;/h3&gt;

&lt;p&gt;With microservices, you can mix and match technology stacks. Each service can use the programming language, database, or framework best suited for its specific requirements. This practice is known as polyglot programming and &lt;a href="https://www.milanjovanovic.tech/blog/modular-monolith-data-isolation" rel="noopener noreferrer"&gt;&lt;strong&gt;polyglot persistence&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, a recommendation engine might use Python with specialized machine learning libraries. On the other hand, a transaction processing service might use .NET for its strong typing and performance characteristics.&lt;/p&gt;

&lt;p&gt;A reporting service might use a columnar database optimized for analytics, while a user profile service could use a document database that better fits its data model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl30dv6f2fgc0xsg1f69j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl30dv6f2fgc0xsg1f69j.png" alt="Image description" width="639" height="746"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This flexibility allows teams to choose the right tool for each job rather than compromising on a one-size-fits-all approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel Development
&lt;/h3&gt;

&lt;p&gt;Multiple teams can work on different services simultaneously without stepping on each other's toes. This parallelization can significantly accelerate development velocity in larger organizations.&lt;/p&gt;

&lt;p&gt;Each team can maintain its own release schedule, make technology decisions, and optimize for their specific service's needs without coordinating with every other team. I've seen firsthand how this autonomy can reduce dependencies between teams, minimizing bottlenecks and wait times.&lt;/p&gt;

&lt;p&gt;Organizations commonly structure their teams around services or groups of related services. You might know this concept as &lt;a href="https://en.wikipedia.org/wiki/Conway%27s_law" rel="noopener noreferrer"&gt;Conway's Law&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Organizations, who design systems, are constrained to produce designs which are copies of the communication structures of these organizations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Targeted Scaling
&lt;/h3&gt;

&lt;p&gt;You can scale just the services that need it, rather than scaling the entire system. This provides more efficient resource utilization and can reduce operational costs.&lt;/p&gt;

&lt;p&gt;For example, if your product catalog needs to handle high traffic during a sale, you can scale just the catalog service without scaling your payment processing service.&lt;/p&gt;

&lt;p&gt;This granular scaling becomes especially valuable as systems grow and different components have different performance characteristics. Some services might be CPU-intensive while others are memory-intensive, and with microservices, you can optimize the infrastructure for each service's specific needs.&lt;/p&gt;

&lt;p&gt;This approach can lead to significant cost savings compared to &lt;a href="https://www.milanjovanovic.tech/blog/scaling-monoliths-a-practical-guide-for-growing-systems" rel="noopener noreferrer"&gt;&lt;strong&gt;scaling a monolith&lt;/strong&gt;&lt;/a&gt;, where all components must scale together regardless of their individual requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Organizational Alignment
&lt;/h3&gt;

&lt;p&gt;Microservices can help align your technical architecture with your organizational structure. Teams can own specific services that correspond to their business domain expertise, promoting clearer ownership and accountability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhzjmhv22wz5xw21wohs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhzjmhv22wz5xw21wohs.png" alt="Image description" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This alignment reduces handoffs and coordination costs between teams, as each team has clear boundaries of responsibility. It supports Conway's Law in a positive way. Instead of having your communication structure accidentally create your architecture, you deliberately design both your teams and your services around business capabilities.&lt;/p&gt;

&lt;p&gt;This approach can lead to more stable team structures and software boundaries over time, as business domains tend to evolve more slowly than technical implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges to Consider With Microservices
&lt;/h2&gt;

&lt;p&gt;While microservices offer numerous benefits, they're not without challenges:&lt;/p&gt;

&lt;h3&gt;
  
  
  Distributed System Complexity
&lt;/h3&gt;

&lt;p&gt;Network communication introduces latency, reliability challenges, and makes debugging more difficult. Services must handle network failures gracefully, implement retries with backoff strategies, and deal with the reality that a request might succeed but the response might get lost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/blog/introduction-to-distributed-tracing-with-opentelemetry-in-dotnet" rel="noopener noreferrer"&gt;&lt;strong&gt;Distributed tracing&lt;/strong&gt;&lt;/a&gt; becomes essential to understand how requests flow through the system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oohn1km2emz2ox4pofo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4oohn1km2emz2ox4pofo.png" alt="Image description" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll need to develop strategies for handling partial system failures. Concepts like circuit breakers and bulkheads become part of your everyday vocabulary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Overhead
&lt;/h3&gt;

&lt;p&gt;Managing many services requires robust &lt;a href="https://www.milanjovanovic.tech/blog/streamlining-dotnet-9-deployment-with-github-actions-and-azure" rel="noopener noreferrer"&gt;&lt;strong&gt;deployment pipelines&lt;/strong&gt;&lt;/a&gt;, monitoring, and debugging tools. You'll need to invest in automation for deployment, &lt;a href="https://www.milanjovanovic.tech/blog/health-checks-in-asp-net-core" rel="noopener noreferrer"&gt;&lt;strong&gt;health checking&lt;/strong&gt;&lt;/a&gt;, scaling, and perhaps &lt;a href="https://www.milanjovanovic.tech/blog/how-dotnet-aspire-simplifies-service-discovery" rel="noopener noreferrer"&gt;&lt;strong&gt;service discovery&lt;/strong&gt;&lt;/a&gt;. Each service needs monitoring, logging, and alerting.&lt;/p&gt;

&lt;p&gt;This overhead can be substantial. Organizations successfully running microservices typically have a strong DevOps culture and tooling to manage this complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Consistency
&lt;/h3&gt;

&lt;p&gt;Maintaining consistency across service boundaries becomes more challenging without the safety of &lt;a href="https://www.milanjovanovic.tech/blog/working-with-transactions-in-ef-core" rel="noopener noreferrer"&gt;&lt;strong&gt;database transactions&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Implementing business processes that span multiple services often requires eventual consistency models and compensation mechanisms to handle failures. You'll need to design your services with &lt;a href="https://www.milanjovanovic.tech/blog/implementing-idempotent-rest-apis-in-aspnetcore" rel="noopener noreferrer"&gt;&lt;strong&gt;idempotency&lt;/strong&gt;&lt;/a&gt; in mind and may need to implement patterns like the&lt;a href="https://www.milanjovanovic.tech/blog/implementing-the-saga-pattern-with-masstransit" rel="noopener noreferrer"&gt;&lt;strong&gt;Saga pattern&lt;/strong&gt;&lt;/a&gt; to manage distributed transactions.&lt;/p&gt;

&lt;p&gt;These approaches add complexity but can actually lead to more resilient systems when implemented correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Coordination
&lt;/h3&gt;

&lt;p&gt;Orchestrating workflows that span multiple services requires careful design. Simple processes in a monolith can become complex choreographies in a microservice architecture.&lt;/p&gt;

&lt;p&gt;You'll need to decide whether to use &lt;a href="https://www.milanjovanovic.tech/blog/orchestration-vs-choreography" rel="noopener noreferrer"&gt;&lt;strong&gt;orchestration&lt;/strong&gt;&lt;/a&gt; (where a central service directs the process) or &lt;a href="https://www.milanjovanovic.tech/blog/orchestration-vs-choreography" rel="noopener noreferrer"&gt;&lt;strong&gt;choreography&lt;/strong&gt;&lt;/a&gt; (where services react to events without central coordination). These patterns have different trade-offs in terms of coupling, resilience, and observability.&lt;/p&gt;

&lt;p&gt;Designing these cross-service workflows often reveals subdomain boundaries you might have missed in initial modeling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;From my experience, the most important thing to remember is that microservices ultimately &lt;strong&gt;buy you options&lt;/strong&gt;. They provide flexibility but come with costs.&lt;/p&gt;

&lt;p&gt;Are these costs worth the options you want to exercise?&lt;/p&gt;

&lt;p&gt;For organizations with a large engineering team working on a complex system that needs to evolve quickly, microservices may be worth the overhead. For smaller teams or systems with more stable requirements, a &lt;a href="https://www.milanjovanovic.tech/blog/what-is-a-modular-monolith" rel="noopener noreferrer"&gt;&lt;strong&gt;well-designed monolith&lt;/strong&gt;&lt;/a&gt; might be more appropriate. Forget about resume-driven development for a moment. It's about what solves your specific problems with &lt;strong&gt;acceptable trade-offs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In my work, I've found that the most successful microservice adoptions start small, often by breaking off just one or two services from a monolith. Then, gradually expanding as the organization builds the necessary skills and infrastructure. This evolutionary approach reduces risk and allows teams to learn as they go.&lt;/p&gt;

&lt;p&gt;I always like to reflect on these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What parts of the current architecture would benefit most from independent deployability?&lt;/li&gt;
&lt;li&gt;What challenges might the organization face when adopting microservices?&lt;/li&gt;
&lt;li&gt;How well does the current system align with business domains?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to dive deeper into building microservices - but starting from a monolith, check out &lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Modular Monolith Architecture&lt;/strong&gt;&lt;/a&gt;. There's an entire chapter dedicated to developing microservices, including advanced techniques such as API gateways, using message queues, and system integration testing.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;p&gt;And stay awesome!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 4,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>microservices</category>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>scalability</category>
    </item>
    <item>
      <title>What is Vector Search? A Concise Guide</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 12 Apr 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/what-is-vector-search-a-concise-guide-1be6</link>
      <guid>https://dev.to/milanjovanovictech/what-is-vector-search-a-concise-guide-1be6</guid>
      <description>&lt;p&gt;&lt;strong&gt;Vector search&lt;/strong&gt; is changing how we find information. Unlike old search methods that look for exact words, vector search finds content based on meaning. This makes search results more helpful and human-like.&lt;/p&gt;

&lt;p&gt;When you search for "quick healthy breakfast ideas," vector search can find articles about "nutritious morning meals" even if they don't use your exact words. This happens because vector search understands what you mean, not just what you type.&lt;/p&gt;

&lt;p&gt;In this week's newsletter, we'll break down how vector search works and why it's important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Vector Embeddings
&lt;/h2&gt;

&lt;p&gt;At the heart of vector search are &lt;strong&gt;vector embeddings&lt;/strong&gt;. These are lists of numbers that represent data. But how do we get from words or images to numbers?&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We feed text, images, or other data into a &lt;a href="https://www.milanjovanovic.tech/blog/working-with-llms-in-dotnet-using-microsoft-extensions-ai" rel="noopener noreferrer"&gt;&lt;strong&gt;large language model (LLM)&lt;/strong&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The LLM turns each piece of data into a list of numbers (a vector)&lt;/li&gt;
&lt;li&gt;These numbers capture the meaning of the data&lt;/li&gt;
&lt;li&gt;Similar things get similar number patterns&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglvogltyque6utwf0si8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglvogltyque6utwf0si8.png" alt="Image description" width="800" height="901"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Think of each number in the vector as describing one aspect of the data. With hundreds or thousands of these numbers, vectors can capture complex meanings and relationships.&lt;/p&gt;

&lt;p&gt;For example, in vector form, the words "lion" and "bobcat" would have similar number patterns because they refer to similar animals. Meanwhile, "cat" would have a different pattern, though still somewhat similar since it's also a feline.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Vector Search Works
&lt;/h2&gt;

&lt;p&gt;When you search using vector search:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your search question gets turned into a vector&lt;/li&gt;
&lt;li&gt;The system compares this vector to all the vectors in its database&lt;/li&gt;
&lt;li&gt;It finds the vectors that are most similar to your search vector&lt;/li&gt;
&lt;li&gt;It returns the data connected to those similar vectors&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To find similar vectors, the system measures how close they are to each other. Think of each vector as a point in space - the closer two points are, the more similar their meanings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9wrsucrpuybd5he4awc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9wrsucrpuybd5he4awc.png" alt="Image description" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector Databases
&lt;/h3&gt;

&lt;p&gt;To make vector search work well with lots of data, we need special storage systems called &lt;strong&gt;vector databases&lt;/strong&gt;. These databases are built to store and quickly search through millions or billions of vectors.&lt;/p&gt;

&lt;p&gt;Vector databases do more than just store vectors - they organize them in smart ways that make searching faster. They use special methods called "Approximate Nearest Neighbor" (ANN) algorithms that can find similar vectors without checking every single one in the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2u9fndnl6vwwcskgjtrb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2u9fndnl6vwwcskgjtrb.png" alt="Image description" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: What are Vector Databases? A Beginner's&lt;br&gt;
Guide&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These databases also store the original content along with its vector, so when you get search results, you see the actual text, images, or other data you were looking for. Popular vector databases include Weaviate, Pinecone, and Qdrant. But you can also turn PostgreSQL into a vector database using the pgvector extension.&lt;/p&gt;

&lt;p&gt;The combination of vector embeddings and vector databases makes search extremely fast, even with millions of items to search through. This speed makes vector search practical for real-world applications where users expect instant results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Differences from Traditional Search
&lt;/h2&gt;

&lt;p&gt;Traditional keyword search works like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You type "red shoes"&lt;/li&gt;
&lt;li&gt;The system finds pages with the words "red" and "shoes"&lt;/li&gt;
&lt;li&gt;Results that mention these words more often rank higher&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach has problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It misses related terms ("scarlet footwear")&lt;/li&gt;
&lt;li&gt;It doesn't understand context&lt;/li&gt;
&lt;li&gt;It can't handle questions well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An improvement to keyword search is &lt;a href="https://www.milanjovanovic.tech/blog/how-i-implemented-full-text-search-on-my-website" rel="noopener noreferrer"&gt;&lt;strong&gt;full-text search&lt;/strong&gt;&lt;/a&gt;, which looks at the whole text of documents. However, this still has some shortcomings that vector search solves.&lt;/p&gt;

&lt;p&gt;Vector search fixes these issues by focusing on meaning rather than exact words. It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find content with related concepts&lt;/li&gt;
&lt;li&gt;Understand the context of your search&lt;/li&gt;
&lt;li&gt;Return helpful results even for complex questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why vector search powers many modern AI applications. It helps chatbots find relevant information and makes recommendation systems more accurate.&lt;/p&gt;

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

&lt;p&gt;Vector search represents a major step forward in how computers understand and retrieve information. By converting data into number patterns that capture meaning, vector search can find connections that keyword search would miss.&lt;/p&gt;

&lt;p&gt;This technology is behind many of the smart search features we now take for granted - from finding similar products in online stores to helping AI assistants answer our questions. As AI continues to advance, vector search will play an increasingly important role in helping us navigate the growing sea of digital information.&lt;/p&gt;

&lt;p&gt;While not perfect, vector search bridges the gap between how computers store data and how humans think about meaning - making our digital tools more helpful and intuitive to use.&lt;/p&gt;

&lt;p&gt;In a future newsletter, we'll dive into the practical side of vector search. You'll learn how to implement your own vector search system using popular tools and libraries, with step-by-step code examples and best practices.&lt;/p&gt;

&lt;p&gt;That's all for today.&lt;/p&gt;

&lt;p&gt;See you next week.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>vectorsearch</category>
      <category>semanticsearch</category>
    </item>
    <item>
      <title>MediatR and MassTransit Going Commerical: What This Means For You</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 05 Apr 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/mediatr-and-masstransit-going-commerical-what-this-means-for-you-2ppi</link>
      <guid>https://dev.to/milanjovanovictech/mediatr-and-masstransit-going-commerical-what-this-means-for-you-2ppi</guid>
      <description>&lt;p&gt;Big changes are happening in the .NET ecosystem. Three powerhouse libraries - MediatR, AutoMapper, and MassTransit - are moving to commercial licenses. Not so long ago, Fluent Assertions also announced its plans to move to a commercial license.&lt;/p&gt;

&lt;p&gt;As someone who's built countless systems with these tools over the past decade, I have thoughts. And some strong opinions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Libraries We Love (And Sometimes Hate)
&lt;/h2&gt;

&lt;p&gt;If you're a .NET developer, you likely use at least one of these:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AutoMapper/AutoMapper" rel="noopener noreferrer"&gt;&lt;strong&gt;AutoMapper&lt;/strong&gt;&lt;/a&gt; (794.7M downloads) transforms objects from one type to another. It removes mountains of tedious mapping code that nobody enjoys writing. One line replaces twenty. &lt;strong&gt;I personally despise AutoMapper and mapping libraries in general&lt;/strong&gt; , but I can't deny their popularity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jbogard/MediatR" rel="noopener noreferrer"&gt;&lt;strong&gt;MediatR&lt;/strong&gt;&lt;/a&gt; (286.6M downloads) implements the &lt;a href="https://www.milanjovanovic.tech/blog/cqrs-pattern-with-mediatr" rel="noopener noreferrer"&gt;&lt;strong&gt;mediator pattern&lt;/strong&gt;&lt;/a&gt;. It decouples requests from the objects handling them, promoting separation of concerns. There's also the pipeline behavior feature, which allows you to add cross-cutting concerns. I'm a huge fan and use it regularly in my projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/MassTransit/MassTransit" rel="noopener noreferrer"&gt;&lt;strong&gt;MassTransit&lt;/strong&gt;&lt;/a&gt; (130.0M downloads) makes distributed messaging simple. It wraps message brokers like &lt;a href="https://www.milanjovanovic.tech/blog/using-masstransit-with-rabbitmq-and-azure-service-bus" rel="noopener noreferrer"&gt;&lt;strong&gt;RabbitMQ and Azure Service Bus&lt;/strong&gt;&lt;/a&gt; with an elegant API. Building event-driven systems becomes approachable. This is another tool I love and often recommend.&lt;/p&gt;

&lt;p&gt;These libraries aren't just popular - they're transformative. They've shaped how we build .NET applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Maintainer's Reality
&lt;/h2&gt;

&lt;p&gt;Both announcements tell a similar story.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2f1q49viz6iu2tj6t2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2f1q49viz6iu2tj6t2z.png" alt="Image description" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: AutoMapper and MediatR Going&lt;br&gt;
Commercial&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Jimmy Bogard (AutoMapper, MediatR) writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can see exactly where my contributions cratered and flat-lined. And that's just commits—issues, PRs, discussions, all my time dried up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;His OSS work was previously sponsored by his former employer. When he went independent, that support vanished. His focus shifted to his consulting business.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3qc82ydqs34kujkq9agk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3qc82ydqs34kujkq9agk.png" alt="Image description" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: Announcing MassTransit&lt;br&gt;
v9&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Similarly, MassTransit has grown from "a single assembly that supported MSMQ" in 2007 to over thirty NuGet packages. Its success created demands that are impossible to meet through volunteer work alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full-time development resources&lt;/li&gt;
&lt;li&gt;Enterprise-grade support&lt;/li&gt;
&lt;li&gt;Long-term sustainability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both maintainers face the same dilemma: how do you support widely used libraries when nobody pays you to do it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Commercial Transition
&lt;/h2&gt;

&lt;p&gt;Here's what's happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AutoMapper and MediatR&lt;/strong&gt; : Jimmy hasn't shared specific timing or pricing yet. He states, "Short term, nothing will change."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MassTransit&lt;/strong&gt; : Moving from v8 (open source) to v9 (commercial) with this timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Q3 2025: v9 prerelease for early adopters&lt;/li&gt;
&lt;li&gt;Q1 2026: v9 official release under commercial license&lt;/li&gt;
&lt;li&gt;Through 2026: v8 security patches continue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MassTransit's pricing targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small/medium businesses: $400/month or $4000/year&lt;/li&gt;
&lt;li&gt;Large enterprises: $1200/month or $12000/year&lt;/li&gt;
&lt;li&gt;Support for ISVs and consultants who build client applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is set in stone. The pricing aspect should be final by the time the commercial version is released.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Respect This Decision
&lt;/h2&gt;

&lt;p&gt;Both maintainers waited over a decade before making this move. They've contributed immense value to our community for free.&lt;/p&gt;

&lt;p&gt;Their announcements show careful consideration. They're not abandoning users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing versions remain open source&lt;/li&gt;
&lt;li&gt;Security patches will continue&lt;/li&gt;
&lt;li&gt;Commercial licenses support sustainable development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I honestly hope none of the above changes in the future. The work they do is valuable.&lt;/p&gt;

&lt;p&gt;Writing these libraries from scratch would cost your team far more than their license fees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Options Now (With My Take)
&lt;/h2&gt;

&lt;p&gt;If your project uses these libraries, you have choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Purchase the commercial license&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&amp;lt;!-- --&amp;gt;This supports continued development and gets you new features and official support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stay on the current open source version&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&amp;lt;!-- --&amp;gt;MassTransit v8 and current MediatR/AutoMapper will remain available. Security patches will continue through 2026 for MassTransit. For MassTransit specifically, I'd consider staying on v8 for the long term if possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Switch to alternatives&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&amp;lt;!-- --&amp;gt;For AutoMapper: consider &lt;a href="https://github.com/MapsterMapper/Mapster" rel="noopener noreferrer"&gt;Mapster&lt;/a&gt; or &lt;strong&gt;manual mapping&lt;/strong&gt; (my recommendation).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&amp;lt;!-- --&amp;gt;For MediatR: explore FastEndpoints or build a simple mediator yourself.&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- --&amp;gt;For MassTransit: look at raw client libraries like RabbitMQ.Client and Azure.Messaging.ServiceBus, and another option to consider is Rebus.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write equivalent functionality yourself&lt;/strong&gt;
&amp;lt;!-- --&amp;gt;MediatR isn't too complex to build on your own. I recommend giving it a try as an excellent coding exercise - it's probably the simplest way to move away from MediatR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For AutoMapper, many teams have deep integrations with business logic in custom mappers. This makes extracting and replacing it difficult. Expect significant tech debt if you don't address this.&lt;/p&gt;

&lt;p&gt;MassTransit, on the other hand, does so many things (and does them well) that migrating away would be challenging. Saga support or the request-response messaging features are hard to replicate. The only real alternative is diving into raw client libraries for your chosen message transport.&lt;/p&gt;

&lt;p&gt;Each option involves tradeoffs. The right choice depends on your project needs and budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Shift to Fundamentals
&lt;/h2&gt;

&lt;p&gt;These changes have made me reflect on something important: we should never lose sight of fundamentals.&lt;/p&gt;

&lt;p&gt;We've been pampered and spoiled by these awesome libraries. It's easy to lose sight of the actual problems they're solving and how they work under the hood. People know how to use MediatR, but they don't understand the mechanisms behind it.&lt;/p&gt;

&lt;p&gt;The same goes for MassTransit. It abstracts away so many complexities of working with message brokers that it's possible to use it without knowing how RabbitMQ or Azure Service Bus actually works.&lt;/p&gt;

&lt;p&gt;Remember this: using a library that abstracts something away doesn't excuse you from understanding the patterns and tools you're using. The &lt;strong&gt;fundamentals are still there&lt;/strong&gt; and always have been. This might be the perfect opportunity to deepen your knowledge of what's happening beneath these abstractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reality Check
&lt;/h2&gt;

&lt;p&gt;Open source isn't free. Someone pays - either with time or money.&lt;/p&gt;

&lt;p&gt;I'm honestly tired of seeing developers complain about these changes. Who are they to demand software for free? The entitlement is astounding. These maintainers have provided immense value for over a decade without asking for anything in return.&lt;/p&gt;

&lt;p&gt;We've enjoyed years of exceptional tooling without directly funding it. Now we face a reckoning.&lt;/p&gt;

&lt;p&gt;As businesses reap massive productivity gains from these libraries, it's reasonable to ask: shouldn't some of that value flow back to the creators?&lt;/p&gt;

&lt;p&gt;I hope these projects thrive under their new models. They've earned support after years of thankless work.&lt;/p&gt;

&lt;p&gt;Both my &lt;a href="https://www.milanjovanovic.tech//pragmatic-clean-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Pragmatic Clean Architecture&lt;/strong&gt;&lt;/a&gt; and&lt;a href="https://www.milanjovanovic.tech//modular-monolith-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Modular Monolith Architecture&lt;/strong&gt;&lt;/a&gt; courses currently use MediatR and MassTransit extensively. I plan to keep them on MediatR v12 and MassTransit v8 in the short term. However, I'll also be updating them to show migration paths away from these libraries.&lt;/p&gt;

&lt;p&gt;What's your take? Will you stick with the open versions, pay for licenses, or explore alternatives?&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;p&gt;And stay awesome!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>mediatr</category>
      <category>masstransit</category>
    </item>
    <item>
      <title>How .NET Aspire Simplifies Service Discovery</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 29 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/how-net-aspire-simplifies-service-discovery-adf</link>
      <guid>https://dev.to/milanjovanovictech/how-net-aspire-simplifies-service-discovery-adf</guid>
      <description>&lt;p&gt;Unless you've been living under a rock, you know that &lt;a href="https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview" rel="noopener noreferrer"&gt;.NET Aspire&lt;/a&gt; is changing (for the better) how we build distributed applications in .NET. A simple way to think about Aspire: it makes all the difficult things in software development easy.&lt;/p&gt;

&lt;p&gt;.NET Aspire is a cloud-native application stack that simplifies the development and deployment of distributed applications. One of the key challenges when building multi-service applications is building reliable &lt;a href="https://www.milanjovanovic.tech/blog/modular-monolith-communication-patterns" rel="noopener noreferrer"&gt;&lt;strong&gt;communication between services&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this week's newsletter, I want to focus on one aspect of .NET Aspire - &lt;a href="https://www.milanjovanovic.tech/blog/service-discovery-in-microservices-with-net-and-consul" rel="noopener noreferrer"&gt;&lt;strong&gt;service discovery&lt;/strong&gt;&lt;/a&gt;. Service discovery lets our services figure out how to locate other services they want to integrate with. .NET Aspire tackles this challenge with a simple, configuration-based approach that reduces complexity and boilerplate code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Service Discovery
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/service-discovery" rel="noopener noreferrer"&gt;&lt;strong&gt;Service discovery&lt;/strong&gt;&lt;/a&gt;is the process by which services in a distributed application locate and communicate with each other. As applications scale and evolve, keeping track of service endpoints becomes increasingly challenging. Services might run on different ports during development or be deployed to different environments in production, making hard-coded service URLs impractical.&lt;/p&gt;

&lt;p&gt;Traditional approaches to service discovery often introduce complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual configuration of service endpoints that must be updated as environments change&lt;/li&gt;
&lt;li&gt;Complex intermediary systems that require additional maintenance&lt;/li&gt;
&lt;li&gt;Custom code to handle service resolution and connection management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While many service discovery implementations rely on centralized registries, .NET Aspire takes a different approach by leveraging application configuration to connect services. This design choice simplifies the development experience while maintaining flexibility for various deployment scenarios. Aspire automatically takes care of wiring up the correct service URLs and injecting them into your application settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkea4dngf5cvavcj0hh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkea4dngf5cvavcj0hh4.png" alt="Image description" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Example of Service Discovery
&lt;/h2&gt;

&lt;p&gt;To understand how .NET Aspire handles service discovery, let's look at a practical example of an application with multiple services:&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="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;DistributedApplication&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;// Add services to the app&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiService&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WeatherApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather-api"&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;webFrontend&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebFrontend&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"web-frontend"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiService&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="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this App Host definition, we're creating two services: a weather API and a web frontend. The &lt;code&gt;.WithReference()&lt;/code&gt; method establishes a connection between these services, which enables service discovery. This simple declaration tells &lt;a href="https://www.milanjovanovic.tech/blog/dotnet-aspire-a-game-changer-for-cloud-native-development" rel="noopener noreferrer"&gt;&lt;strong&gt;.NET Aspire&lt;/strong&gt;&lt;/a&gt; that the web frontend depends on the weather API and needs to communicate with it. Note that the &lt;code&gt;web-frontend&lt;/code&gt; has to be a server-side application (like &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models" rel="noopener noreferrer"&gt;Blazor Server&lt;/a&gt;) for Aspire to be able to inject the service URL.&lt;/p&gt;

&lt;p&gt;With this configuration in place, the web frontend can now reach the API using the service name &lt;code&gt;weather-api&lt;/code&gt;without additional service discovery code:&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="c1"&gt;// Configures the default Aspire services, including service discovery&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;AddServiceDefaults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// In Program.cs of the web-frontend project&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;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&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;// The service name "weather-api" automatically resolves to the correct address&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://weather-api"&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;This works because .NET Aspire manages the mapping between service names and their actual endpoints. When the application runs, service discovery ensures that requests to &lt;code&gt;http://weather-api&lt;/code&gt; are routed to the appropriate destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Discovery Under the Hood
&lt;/h2&gt;

&lt;p&gt;The previous example contains a bit of "magic" that might not be immediately clear. The &lt;code&gt;AddServiceDefaults()&lt;/code&gt; method configures the default services for the application, including service discovery.&lt;/p&gt;

&lt;p&gt;If we were to configure everything manually, it would look something like this:&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;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;AddServiceDiscovery&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;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://weather-api"&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;AddServiceDiscovery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;AddServiceDiscovery()&lt;/code&gt; method registers the necessary services to enable service discovery in the application. The second &lt;code&gt;AddServiceDiscovery()&lt;/code&gt; method on the HTTP client configures it to use service discovery for resolving the base address. This means that when the HTTP client makes requests to &lt;code&gt;http://weather-api&lt;/code&gt;, it will automatically resolve the correct endpoint based on the service discovery configuration.&lt;/p&gt;

&lt;p&gt;We can also configure service discovery globally for all HTTP clients in the application:&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;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;ConfigureHttpClientDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;http&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;// Turn on service discovery by default&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddServiceDiscovery&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;This configuration ensures that all HTTP clients in the application will use service discovery by default, eliminating the need to configure it for each client individually.&lt;/p&gt;

&lt;p&gt;What Aspire does at runtime for all of this to work is inject a set of configuration values. The configuration value names are derived from the service names we defined in the App Host, plus the respective scheme (http or https). Here's an example of what the &lt;code&gt;weather-api&lt;/code&gt; configuration might look like:&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="s"&gt;"Services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"weather-api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"http"&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:8080"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also configure service discovery to work with HTTPS by adding the &lt;code&gt;https&lt;/code&gt; scheme to the service name:&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="c1"&gt;// Specify the https scheme explicitly in the service name&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;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://weather-api"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Alternatively, we can use the https+http scheme and let Aspire handle the conversion&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;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather-api-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https+http://weather-api"&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;A significant advantage of Aspire's service discovery is its consistent behavior across environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During development, services might run on localhost with different ports&lt;/li&gt;
&lt;li&gt;In testing environments, services could be containerized&lt;/li&gt;
&lt;li&gt;In production, services might be deployed to Kubernetes or other platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your code remains unchanged across these scenarios because the service name abstraction shields you from the underlying networking details.&lt;/p&gt;

&lt;p&gt;Note that you don't have to use .NET Aspire to benefit from &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/service-discovery" rel="noopener noreferrer"&gt;service discovery&lt;/a&gt;. It's available as a standalone library (&lt;code&gt;Microsoft.Extensions.ServiceDiscovery&lt;/code&gt;) and you can use it in any .NET application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Discovery with YARP as a Proxy
&lt;/h2&gt;

&lt;p&gt;A powerful application of .NET Aspire's service discovery capabilities is in API gateway scenarios using&lt;a href="https://www.milanjovanovic.tech/blog/implementing-an-api-gateway-for-microservices-with-yarp" rel="noopener noreferrer"&gt;&lt;strong&gt;YARP&lt;/strong&gt;&lt;/a&gt; (Yet Another Reverse Proxy). Let's explore how to implement this pattern:&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="c1"&gt;// In the App Host&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiService&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WeatherApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather-api"&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;userService&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserApi&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"user-api"&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;proxyService&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiGateway&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"api-gateway"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll need to add the YARP NuGet package to the API gateway project:&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;Install&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Package&lt;/span&gt; &lt;span class="n"&gt;Yarp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReverseProxy&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Adds&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;YARP&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;
&lt;span class="n"&gt;Install&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Package&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceDiscovery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Yarp&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Adds&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;YARP&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="n"&gt;discovery&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the gateway project, we can configure YARP to use service discovery:&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="c1"&gt;// In Program.cs of the api-gateway project&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;// Cofigures the service discovery services&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;AddServiceDiscovery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Add YARP services&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;AddReverseProxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadFromConfig&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;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReverseProxy"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;// Configures a destination resolver that can use service discovery&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddServiceDiscoveryDestinationResolver&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="c1"&gt;// Configure the HTTP request pipeline&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;MapReverseProxy&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;p&gt;The YARP configuration in &lt;code&gt;appsettings.json&lt;/code&gt; leverages the service names for endpoint resolution:&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="s"&gt;"ReverseProxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"Routes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"weather-route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"weather-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/weather/{**catch-all}"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"Transforms"&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="s"&gt;"PathRemovePrefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/weather"&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="s"&gt;"user-route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"user-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/users/{**catch-all}"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"Transforms"&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="s"&gt;"PathRemovePrefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/users"&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="p"&gt;},&lt;/span&gt;
    &lt;span class="s"&gt;"Clusters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"weather-cluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"Destinations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"destination1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"http://weather-api"&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="s"&gt;"user-cluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"Destinations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"destination1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"http://user-api"&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="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;This configuration creates an API gateway that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes requests with path &lt;code&gt;/weather/*&lt;/code&gt; to the weather API service&lt;/li&gt;
&lt;li&gt;Routes requests with path &lt;code&gt;/users/*&lt;/code&gt; to the user API service&lt;/li&gt;
&lt;li&gt;Uses service names (&lt;code&gt;weather-api&lt;/code&gt; and &lt;code&gt;user-api&lt;/code&gt;) that service discovery resolves at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The beauty of this approach is that the gateway doesn't need to know the actual endpoints of the backend services. It simply uses the service names, and .NET Aspire handles providing the configuration at runtime. This makes the gateway configuration more portable and easier to maintain as the application evolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;.NET Aspire transforms service discovery from a complex infrastructure challenge into a straightforward configuration concern. By using a configuration-based approach rather than a centralized registry, it simplifies the development experience while maintaining the flexibility needed for various deployment scenarios.&lt;/p&gt;

&lt;p&gt;The key advantages of Aspire's service discovery include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Declarative service relationships in the App Host&lt;/li&gt;
&lt;li&gt;Simple service name resolution that works across environments&lt;/li&gt;
&lt;li&gt;Seamless integration with the .NET ecosystem and dependency injection&lt;/li&gt;
&lt;li&gt;Powerful applications in patterns like API gateways with YARP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the .NET Aspire stack continues to evolve, its approach to service discovery represents one of the ways it's making cloud-native development more accessible to .NET developers. By reducing the complexity of service-to-service communication, Aspire enables teams to focus on building features rather than wrestling with infrastructure concerns.&lt;/p&gt;

&lt;p&gt;If you want to explore more robust service discovery solutions for large-scale distributed systems, check out my previous article on &lt;a href="https://www.milanjovanovic.tech/blog/service-discovery-in-microservices-with-net-and-consul" rel="noopener noreferrer"&gt;&lt;strong&gt;implementing service discovery with Consul&lt;/strong&gt;&lt;/a&gt;. It provides a complementary approach for scenarios that might require a more traditional service registry.&lt;/p&gt;

&lt;p&gt;For those who found the YARP integration particularly interesting, my &lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture" rel="noopener noreferrer"&gt;&lt;strong&gt;Modular Monolith Architecture&lt;/strong&gt;&lt;/a&gt; course dives deeper into building scalable applications with YARP as an API gateway. You'll learn how to leverage these patterns to create maintainable, evolvable systems regardless of whether you're using microservices or a monolith.&lt;/p&gt;

&lt;p&gt;That's all for today.&lt;/p&gt;

&lt;p&gt;See you next Saturday.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aspire</category>
      <category>cloudnative</category>
      <category>microservices</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Options Pattern Validation in ASP.NET Core With FluentValidation</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 22 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/options-pattern-validation-in-aspnet-core-with-fluentvalidation-idg</link>
      <guid>https://dev.to/milanjovanovictech/options-pattern-validation-in-aspnet-core-with-fluentvalidation-idg</guid>
      <description>&lt;p&gt;If you've worked with the &lt;a href="https://www.milanjovanovic.tech/blog/how-to-use-the-options-pattern-in-asp-net-core-7" rel="noopener noreferrer"&gt;&lt;strong&gt;Options Pattern&lt;/strong&gt;&lt;/a&gt; in ASP.NET Core, you're likely familiar with the built-in validation using &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-9.0#validation-attributes" rel="noopener noreferrer"&gt;Data Annotations&lt;/a&gt;. While functional, Data Annotations can be limiting for complex validation scenarios.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Options Pattern&lt;/strong&gt; lets you use classes to obtain strongly typed configuration objects at runtime.&lt;/p&gt;

&lt;p&gt;The problem? You can't be certain that the configuration is valid until you try to use it.&lt;/p&gt;

&lt;p&gt;So why not validate it at application startup?&lt;/p&gt;

&lt;p&gt;In this article, we'll explore how to integrate the more powerful &lt;a href="https://docs.fluentvalidation.net/en/latest/" rel="noopener noreferrer"&gt;FluentValidation&lt;/a&gt;library with ASP.NET Core's &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options" rel="noopener noreferrer"&gt;Options Pattern&lt;/a&gt;, to build a robust validation solution that executes at application startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FluentValidation Over Data Annotations?
&lt;/h2&gt;

&lt;p&gt;Data Annotations work well for simple validations, but FluentValidation offers several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More expressive and flexible validation rules&lt;/li&gt;
&lt;li&gt;Better support for complex conditional validations&lt;/li&gt;
&lt;li&gt;Cleaner separation of concerns (validation logic separate from model)&lt;/li&gt;
&lt;li&gt;Easier testing of validation rules&lt;/li&gt;
&lt;li&gt;Better support for custom validation logic&lt;/li&gt;
&lt;li&gt;Allows for injecting dependencies into validators&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding the Options Pattern Lifecycle
&lt;/h2&gt;

&lt;p&gt;Before diving deep into validation, it's important to understand the lifecycle of options in ASP.NET Core:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Options are registered with the DI container&lt;/li&gt;
&lt;li&gt;Configuration values are bound to options classes&lt;/li&gt;
&lt;li&gt;Validation occurs (if configured)&lt;/li&gt;
&lt;li&gt;Options are resolved when requested via &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt;, or &lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;ValidateOnStart()&lt;/code&gt; method forces validation to occur during application startup rather than when options are first resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Configuration Failures Without Validation
&lt;/h2&gt;

&lt;p&gt;Without validation, configuration issues can manifest in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures&lt;/strong&gt; : An incorrectly configured option may result in default values being used without warning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime exceptions&lt;/strong&gt; : Configuration issues may only surface when the application tries to use invalid values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascading failures&lt;/strong&gt; : One misconfigured component can cause failures in dependent systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By validating at startup, you create a fast feedback loop that prevents these issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Foundation
&lt;/h2&gt;

&lt;p&gt;First, let's add the FluentValidation package to our project:&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;Install&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Package&lt;/span&gt; &lt;span class="n"&gt;FluentValidation&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;
&lt;span class="n"&gt;Install&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;Package&lt;/span&gt; &lt;span class="n"&gt;FluentValidation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DependencyInjectionExtensions&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;DI&lt;/span&gt; &lt;span class="n"&gt;integration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our example, we'll use a &lt;code&gt;GitHubSettings&lt;/code&gt; class that requires validation:&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;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubSettings&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;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ConfigurationSection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GitHubSettings"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;init&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;AccessToken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RepositoryName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&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;h2&gt;
  
  
  Creating a FluentValidation Validator
&lt;/h2&gt;

&lt;p&gt;Next, we'll create a validator for our settings class:&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;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubSettingsValidator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbstractValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&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="nf"&gt;GitHubSettingsValidator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UriKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Absolute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&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;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMessage&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;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt; must be a valid URL"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RepositoryName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&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;h2&gt;
  
  
  Building the FluentValidation Integration
&lt;/h2&gt;

&lt;p&gt;To integrate FluentValidation with the Options Pattern, we need to create a custom &lt;code&gt;IValidateOptions&amp;lt;T&amp;gt;&lt;/code&gt; implementation:&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;FluentValidation&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;Microsoft.Extensions.Options&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;FluentValidateOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&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;IValidateOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TOptions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_name&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;FluentValidateOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceProvider&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_serviceProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&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="n"&gt;ValidateOptionsResult&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&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;_name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&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;ValidateOptionsResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&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="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateScope&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;validator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&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;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&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;ValidateOptionsResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&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;type&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="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Name&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;errors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&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="k"&gt;foreach&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;failure&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Validation failed for &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;type&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;failure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropertyName&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="s"&gt;$"with the error: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorMessage&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="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValidateOptionsResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&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;A few important notes about this implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We create a scoped service provider to properly resolve the validator (since validators are typically registered as scoped services)&lt;/li&gt;
&lt;li&gt;We handle named options through the &lt;code&gt;_name&lt;/code&gt; property&lt;/li&gt;
&lt;li&gt;We build informative error messages that include the property name and error message&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How the FluentValidation Integration Works
&lt;/h2&gt;

&lt;p&gt;When adding our custom FluentValidation integration, it's helpful to understand how it connects to ASP.NET Core's options system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;IValidateOptions&amp;lt;T&amp;gt;&lt;/code&gt; interface is the hook that ASP.NET Core provides for options validation&lt;/li&gt;
&lt;li&gt;Our &lt;code&gt;FluentValidateOptions&amp;lt;T&amp;gt;&lt;/code&gt; class implements this interface to bridge to FluentValidation&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;ValidateOnStart()&lt;/code&gt; is called, ASP.NET Core resolves all &lt;code&gt;IValidateOptions&amp;lt;T&amp;gt;&lt;/code&gt; implementations and runs them&lt;/li&gt;
&lt;li&gt;If validation fails, an &lt;code&gt;OptionsValidationException&lt;/code&gt; is thrown, preventing the application from starting&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Creating Extension Methods for Easy Integration
&lt;/h2&gt;

&lt;p&gt;Now, let's create a few extension methods to make our validation easier to use:&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OptionsBuilderExtensions&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;static&lt;/span&gt; &lt;span class="n"&gt;OptionsBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ValidateFluentValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;OptionsBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TOptions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&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="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IValidateOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;serviceProvider&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="n"&gt;FluentValidateOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                &lt;span class="n"&gt;serviceProvider&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;Name&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;builder&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;This extension method allows us to call &lt;code&gt;.ValidateFluentValidation()&lt;/code&gt; when configuring options, similar to the built-in &lt;code&gt;.ValidateDataAnnotations()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;For even more convenience, we can create another extension method to simplify the entire configuration process:&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceCollectionExtensions&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;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;AddOptionsWithFluentValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&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;configurationSection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TOptions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configurationSection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateFluentValidation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Configure FluentValidation validation&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Validate options on application start&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;services&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;h2&gt;
  
  
  Registering and Using the Validation
&lt;/h2&gt;

&lt;p&gt;There are a few ways to use our FluentValidation integration:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Standard Registration with Manual Validator Registration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register the validator&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="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;GitHubSettingsValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Configure options with validation&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="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BindConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigurationSection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateFluentValidation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Configure FluentValidation validation&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2: Using the Convenience Extension Method
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Register the validator&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="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;GitHubSettingsValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Use the convenience extension&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="n"&gt;AddOptionsWithFluentValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigurationSection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Automatic Validator Registration
&lt;/h3&gt;

&lt;p&gt;If you have many validators and want to register them all at once, you can use FluentValidation's assembly scanning:&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="c1"&gt;// Register all validators from assembly&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;AddValidatorsFromAssembly&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="c1"&gt;// Use the convenience extension&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="n"&gt;AddOptionsWithFluentValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigurationSection&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 at Runtime?
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;.ValidateOnStart()&lt;/code&gt;, the application will throw an exception during startup if any validation rules fail. For example, if your &lt;code&gt;appsettings.json&lt;/code&gt; is missing the required &lt;code&gt;AccessToken&lt;/code&gt;, you'll see something like:&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;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extensions&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;OptionsValidationException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;Validation&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;Access&lt;/span&gt; &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents your application from even starting with invalid configuration, ensuring issues are caught as early as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with Different Configuration Sources
&lt;/h2&gt;

&lt;p&gt;ASP.NET Core's configuration system supports multiple sources. When using the Options Pattern with FluentValidation, remember that validation works regardless of the source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Azure Key Vault&lt;/li&gt;
&lt;li&gt;User secrets&lt;/li&gt;
&lt;li&gt;JSON files&lt;/li&gt;
&lt;li&gt;In-memory configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is particularly useful for containerized applications where configuration comes from environment variables or mounted secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Your Validators
&lt;/h2&gt;

&lt;p&gt;One benefit of using FluentValidation is that validators are easy to test:&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="c1"&gt;// Uses helper methods from FluentValidation.TestHelper&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&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;void&lt;/span&gt; &lt;span class="nf"&gt;GitHubSettings_WithMissingAccessToken_ShouldHaveValidationError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;validator&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;GitHubSettingsValidator&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;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GitHubSettings&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;RepositoryName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"test-repo"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&lt;/span&gt;
    &lt;span class="n"&gt;TestValidationResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;result&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;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TestValidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Assert&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldNotHaveAnyValidationErrors&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;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By combining FluentValidation with the Options Pattern and &lt;code&gt;ValidateOnStart()&lt;/code&gt;, we create a powerful &lt;a href="https://www.milanjovanovic.tech/blog/cqrs-validation-with-mediatr-pipeline-and-fluentvalidation" rel="noopener noreferrer"&gt;&lt;strong&gt;validation system&lt;/strong&gt;&lt;/a&gt; that ensures our application has correct configuration at startup.&lt;/p&gt;

&lt;p&gt;This approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provides more expressive validation rules than Data Annotations&lt;/li&gt;
&lt;li&gt;Separates validation logic from configuration models&lt;/li&gt;
&lt;li&gt;Catches configuration errors at application startup&lt;/li&gt;
&lt;li&gt;Supports complex validation scenarios&lt;/li&gt;
&lt;li&gt;Is easily testable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern is particularly valuable in &lt;a href="https://www.milanjovanovic.tech/blog/monolith-to-microservices-how-a-modular-monolith-helps" rel="noopener noreferrer"&gt;&lt;strong&gt;microservice architectures&lt;/strong&gt;&lt;/a&gt; or containerized applications where configuration errors should be detected immediately rather than at runtime.&lt;/p&gt;

&lt;p&gt;Remember to register your validators appropriately and use &lt;code&gt;.ValidateOnStart()&lt;/code&gt; to ensure validation happens during application startup.&lt;/p&gt;

&lt;p&gt;That's all for today.&lt;/p&gt;

&lt;p&gt;See you next Saturday.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>dotnet</category>
      <category>aspnetcore</category>
      <category>optionspattern</category>
      <category>configuration</category>
    </item>
    <item>
      <title>Streamlining .NET 9 Deployment With GitHub Actions and Azure</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 15 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/streamlining-net-9-deployment-with-github-actions-and-azure-ole</link>
      <guid>https://dev.to/milanjovanovictech/streamlining-net-9-deployment-with-github-actions-and-azure-ole</guid>
      <description>&lt;p&gt;I remember the days of deploying .NET applications by hand: publishing locally, copying files to servers, running scripts, and crossing my fingers that nothing would break. It was stressful, time-consuming, and honestly, a bit scary.&lt;/p&gt;

&lt;p&gt;But those days are over.&lt;/p&gt;

&lt;p&gt;After implementing &lt;a href="https://en.wikipedia.org/wiki/CI/CD" rel="noopener noreferrer"&gt;CI/CD&lt;/a&gt; pipelines for dozens of projects, I've seen firsthand how automation transforms the deployment process from a dreaded chore into a reliable, even boring, part of development.&lt;/p&gt;

&lt;p&gt;And boring deployments are good deployments.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through setting up a robust CI/CD pipeline for .NET 9 applications using&lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; and &lt;a href="https://azure.microsoft.com/en-us/products/app-service" rel="noopener noreferrer"&gt;Azure App Service&lt;/a&gt;. I'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What CI/CD is and why it matters for .NET developers&lt;/li&gt;
&lt;li&gt;A complete workflow that builds, tests, and deploys your application&lt;/li&gt;
&lt;li&gt;How to extend your pipeline with database migrations, code coverage, and more&lt;/li&gt;
&lt;li&gt;Practical tips I've learned from real-world deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're tired of manual deployments or looking to improve your existing automation, this guide will help you build a robust CI/CD pipeline that you can easily extend to fit your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CI/CD and Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;CI/CD stands for &lt;strong&gt;Continuous Integration&lt;/strong&gt; and &lt;strong&gt;Continuous Delivery/Deployment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In simple terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Integration (CI)&lt;/strong&gt; means frequently merging code changes and running automated tests to catch issues early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Delivery (CD)&lt;/strong&gt; means getting those changes to production-ready environments quickly and safely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Deployment (CD)&lt;/strong&gt; is an extension of Continuous Delivery where every change that passes automated tests is deployed to production automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main benefits I've seen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Faster feedback&lt;/strong&gt; : Find bugs within minutes instead of days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More stable releases&lt;/strong&gt; : Small, incremental changes are easier to fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time savings&lt;/strong&gt; : Let automation handle repetitive tasks while you focus on writing code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent deployment&lt;/strong&gt; : No more "it works on my machine" problems&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My GitHub Actions Workflow for .NET 9
&lt;/h2&gt;

&lt;p&gt;Here's the workflow I use to deploy a simple time service API to Azure App Service:&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="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;appears&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GitHub&lt;/span&gt; &lt;span class="n"&gt;Actions&lt;/span&gt; &lt;span class="n"&gt;UI&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="n"&gt;CI&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Define&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Allow&lt;/span&gt; &lt;span class="n"&gt;manual&lt;/span&gt; &lt;span class="n"&gt;triggering&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;GitHub&lt;/span&gt; &lt;span class="n"&gt;UI&lt;/span&gt;
  &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="n"&gt;automatically&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;pushed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="n"&gt;throughout&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;AZURE_WEBAPP_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;
  &lt;span class="n"&gt;AZURE_WEBAPP_PACKAGE_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;./&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
  &lt;span class="n"&gt;DOTNET_VERSION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
  &lt;span class="n"&gt;SOLUTION_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sln&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
  &lt;span class="n"&gt;API_PROJECT_PATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
  &lt;span class="n"&gt;PUBLISH_DIR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;./&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Define&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;separate&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;make&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;
&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;First&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;
  &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Build&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;Test&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;Ubuntu&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;

    &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;checkout@v4&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Setup&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NET&lt;/span&gt;
        &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dotnet@v4&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTNET_VERSION&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Restore&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;restore&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOLUTION_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Build&lt;/span&gt;
      &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOLUTION_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="n"&gt;Release&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;restore&lt;/span&gt;

    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Test&lt;/span&gt;
      &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOLUTION_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="n"&gt;Release&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;restore&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;verbosity&lt;/span&gt; &lt;span class="n"&gt;normal&lt;/span&gt;

    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Publish&lt;/span&gt;
      &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;publish&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;API_PROJECT_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="n"&gt;Release&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;restore&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;
        &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;PublishDir&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUBLISH_DIR&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;published&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;later&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Publish&lt;/span&gt; &lt;span class="n"&gt;Artifacts&lt;/span&gt;
      &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;artifact@v4&lt;/span&gt;
      &lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;webapp&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_WEBAPP_PACKAGE_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

  &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt;
  &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;
    &lt;span class="n"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;depends&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;

    &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Retrieve&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;artifacts&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Download&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;
        &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;download&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;artifact@v4&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;webapp&lt;/span&gt;
          &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_WEBAPP_PACKAGE_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

      &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;publish&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt;
        &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;webapps&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;deploy@v2&lt;/span&gt;
        &lt;span class="k"&gt;with&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_WEBAPP_NAME&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Authentication&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;
          &lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_WEBAPP_PUBLISH_PROFILE&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
          &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_WEBAPP_PACKAGE_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow does two main things: it builds and tests the code and then deploys it to Azure.&lt;/p&gt;

&lt;p&gt;The first job checks out our repository, sets up .NET 9, and runs through a standard build process: restore packages, build the solution, run tests, and publish the application. The detailed comments in the YAML explain each step. Once everything passes, it packages the application as an artifact for the next job.&lt;/p&gt;

&lt;p&gt;The second job takes that artifact and deploys it to Azure App Service using a publish profile. I store the publish profile as a &lt;a href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions" rel="noopener noreferrer"&gt;GitHub secret&lt;/a&gt; for security. The &lt;code&gt;needs: [build-and-test]&lt;/code&gt; line ensures deployment only happens if all tests pass, which protects our production environment from broken code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fohgleis6ihce1ibxovhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fohgleis6ihce1ibxovhe.png" alt="Image description" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here's an example of what a workflow run looks like from the GitHub UI.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending Your CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;While the basic workflow gets your application deployed, most real-world projects need more sophisticated pipelines. As your project grows, so should your CI/CD process. Extensions to your pipeline could help catch issues earlier, ensure quality standards, and provide better visibility into your development process.&lt;/p&gt;

&lt;p&gt;Here are some valuable additions to consider:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Running Database Migrations
&lt;/h3&gt;

&lt;p&gt;Database schema changes can be tricky to coordinate with code deployments. There are several approaches to handling this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using EF Core Migration Bundles&lt;/strong&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt; &lt;span class="n"&gt;migration&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;
  &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;ef&lt;/span&gt; &lt;span class="n"&gt;migrations&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATA_PROJECT&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;--&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIGRATIONS_BUNDLE&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;

&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Apply&lt;/span&gt; &lt;span class="n"&gt;migrations&lt;/span&gt;
  &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIGRATIONS_BUNDLE&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/blog/efcore-migrations-a-detailed-guide" rel="noopener noreferrer"&gt;&lt;strong&gt;Migration bundles&lt;/strong&gt;&lt;/a&gt; (introduced in EF Core 6.0) package your migrations into a standalone executable, making them easier to run in deployment pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding Manual Review for Migrations&lt;/strong&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;deploy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt; &lt;span class="n"&gt;Changes&lt;/span&gt;
  &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt;
  &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;
  &lt;span class="n"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach adds an environment with protection rules, requiring a DBA to review and approve migration scripts before they run. This is safer for production databases with valuable data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No manual migration steps&lt;/li&gt;
&lt;li&gt;Schema and code changes deploy together&lt;/li&gt;
&lt;li&gt;Database changes are versioned with code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failed migrations can be hard to roll back&lt;/li&gt;
&lt;li&gt;Might need extra handling for production data&lt;/li&gt;
&lt;li&gt;Requires secure database credentials in CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To minimize risks, I test migrations in a staging environment first and always back up production databases before deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Code Coverage Reports
&lt;/h3&gt;

&lt;p&gt;I like knowing how much of my code is covered by tests. Here's an example of how to generate and publish code coverage reports to &lt;a href="https://about.codecov.io/" rel="noopener noreferrer"&gt;Codecov&lt;/a&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Generate&lt;/span&gt; &lt;span class="n"&gt;coverage&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;
  &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOLUTION_PATH&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;CollectCoverage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;CoverletOutputFormat&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cobertura&lt;/span&gt;

&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Publish&lt;/span&gt; &lt;span class="n"&gt;coverage&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;
  &lt;span class="n"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;codecov&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;codecov&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;action@v5&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="cm"&gt;/**/&lt;/span&gt;&lt;span class="n"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cobertura&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;
    &lt;span class="n"&gt;fail_ci_if_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CODECOV_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding minimum coverage requirements prevents drops in test coverage and encourages the team to maintain quality standards. You can also configure it to fail builds when coverage falls below a threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-Environment Deployment
&lt;/h3&gt;

&lt;p&gt;For larger projects, deploying to multiple environments with approval gates provides better control:&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;deploy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;Staging&lt;/span&gt;
  &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;staging&lt;/span&gt;
  &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;
  &lt;span class="n"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Deployment&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;Production&lt;/span&gt;
  &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt;
  &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ubuntu&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;
  &lt;span class="n"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;staging&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Deployment&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding protection rules to your production environment creates checkpoints where team members can verify changes before they reach users.&lt;/p&gt;

&lt;p&gt;Here's an example of some GitHub Environment protection rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Required reviewers&lt;/strong&gt; : Specify team members who must approve deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait timers&lt;/strong&gt; : Add a delay before deployments to give time for review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment branches&lt;/strong&gt; : Restrict which branches can deploy to production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These guardrails are especially important for critical environments where downtime can be costly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A good CI/CD pipeline evolves with your project. Start simple, focus on automating the most painful manual tasks first, then gradually add more features as needed.&lt;/p&gt;

&lt;p&gt;The initial setup takes time, but the long-term benefits are huge. My team now deploys multiple times per day instead of once every few weeks, with fewer bugs reaching production.&lt;/p&gt;

&lt;p&gt;If you want to learn more about building robust APIs that complement your CI/CD process, check out my &lt;a href="https://www.milanjovanovic.tech/pragmatic-rest-apis" rel="noopener noreferrer"&gt;&lt;strong&gt;Pragmatic REST APIs&lt;/strong&gt;&lt;/a&gt; course. It covers designing, implementing, and deploying production-ready APIs with best practices that work perfectly with the deployment pipeline we've discussed here.&lt;/p&gt;

&lt;p&gt;What's your CI/CD setup like? I'd love to hear how you've customized your workflows for .NET applications.&lt;/p&gt;

&lt;p&gt;That's all for today.&lt;/p&gt;

&lt;p&gt;See you next week.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>dotnet</category>
      <category>deployment</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Better Request Tracing with User Context in ASP.NET Core</title>
      <dc:creator>Milan Jovanović</dc:creator>
      <pubDate>Sat, 08 Mar 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/milanjovanovictech/better-request-tracing-with-user-context-in-aspnet-core-m4h</link>
      <guid>https://dev.to/milanjovanovictech/better-request-tracing-with-user-context-in-aspnet-core-m4h</guid>
      <description>&lt;p&gt;When building web applications, knowing what's happening behind the scenes is crucial. In ASP.NET Core, we can make our lives easier by adding user context to our request tracing. This helps us track issues, understand user behavior, and improve our applications.&lt;/p&gt;

&lt;p&gt;Let me show you how to enhance your request tracing by adding user context in ASP.NET Core applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Add User Context to Request Tracing?
&lt;/h2&gt;

&lt;p&gt;When something goes wrong in your application, having the user's ID in your logs makes it much easier to figure out what happened. Instead of searching through thousands of log entries, you can filter by user ID and see only the relevant logs.&lt;/p&gt;

&lt;p&gt;Adding user context also helps with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tracking user journeys through your application&lt;/li&gt;
&lt;li&gt;Identifying patterns in user behavior&lt;/li&gt;
&lt;li&gt;Troubleshooting issues for specific users&lt;/li&gt;
&lt;li&gt;Monitoring performance for different user segments&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementing User Context Enrichment
&lt;/h2&gt;

&lt;p&gt;The core of our solution is a middleware component that extracts the user ID from the current user's claims and adds it to the current &lt;strong&gt;activity&lt;/strong&gt; and &lt;strong&gt;logging scope&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;logging scope&lt;/strong&gt; in ASP.NET Core lets you attach additional data to all log messages created within that scope.&lt;a href="https://www.milanjovanovic.tech/blog/structured-logging-in-asp-net-core-with-serilog" rel="noopener noreferrer"&gt;&lt;strong&gt;Structured logging&lt;/strong&gt;&lt;/a&gt; frameworks like &lt;a href="https://www.milanjovanovic.tech/blog/5-serilog-best-practices-for-better-structured-logging" rel="noopener noreferrer"&gt;&lt;strong&gt;Serilog&lt;/strong&gt;&lt;/a&gt;and the built-in logger support this feature. For example, if you add a user ID to a logging scope, every log message within that scope will include that user ID, even if the log message itself doesn't mention the user. This makes it easy to correlate logs for a specific user across different parts of your application.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Activity&lt;/code&gt; class is part of the .NET diagnostics infrastructure. It represents a unit of work or operation and is designed for distributed tracing across service boundaries. When you add a tag to &lt;code&gt;Activity.Current&lt;/code&gt;, that information becomes part of the trace and can be used to filter and analyze requests in your monitoring systems.&lt;/p&gt;

&lt;p&gt;Here's the code:&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.Diagnostics&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;System.Security.Claims&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyApp.Middleware&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;UserContextEnrichmentMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;RequestDelegate&lt;/span&gt; &lt;span class="n"&gt;next&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;UserContextEnrichmentMiddleware&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;{&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;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;userId&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;User&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;FindFirstValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClaimTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NameIdentifier&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;userId&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&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="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;SetTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user.id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&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;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&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;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"UserId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="k"&gt;using&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;BeginScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&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="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&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="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;Let's break down what this middleware does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It extracts the user ID from the authenticated user's claims using &lt;code&gt;context.User?.FindFirstValue(ClaimTypes.NameIdentifier)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If a user ID is found, it adds the ID as a tag to the current activity with &lt;code&gt;Activity.Current?.SetTag("user.id", userId)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It creates a logging scope using &lt;code&gt;ILogger.BeginScope&lt;/code&gt; with the user ID, which means all log entries within this scope will include the user ID&lt;/li&gt;
&lt;li&gt;It calls the next middleware in the pipeline&lt;/li&gt;
&lt;li&gt;If no user ID is found (for anonymous requests), it simply calls the next middleware&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Logging Scopes and OpenTelemetry
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;a href="https://www.milanjovanovic.tech/blog/introduction-to-distributed-tracing-with-opentelemetry-in-dotnet" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenTelemetry&lt;/strong&gt;&lt;/a&gt; to export logs, you have to configure the provider to include the log scopes on the generated log records.&lt;/p&gt;

&lt;p&gt;Here's how:&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenTelemetry&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;IncludeScopes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&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;IncludeFormattedMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&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;h2&gt;
  
  
  Adding the Middleware to Your Application
&lt;/h2&gt;

&lt;p&gt;To use this &lt;a href="https://www.milanjovanovic.tech/blog/3-ways-to-create-middleware-in-asp-net-core" rel="noopener noreferrer"&gt;&lt;strong&gt;middleware&lt;/strong&gt;&lt;/a&gt; in your ASP.NET Core application, add it to your pipeline in the &lt;code&gt;Program.cs&lt;/code&gt; file:&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;UseAuthentication&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;UseAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Add the user context enrichment middleware after authentication&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;UseMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserContextEnrichmentMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;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;MapControllers&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;p&gt;Make sure to place it after the authentication and authorization middleware so that the user identity is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling PII Concerns
&lt;/h2&gt;

&lt;p&gt;When adding user IDs to your logs and traces, you need to be careful about &lt;a href="https://en.wikipedia.org/wiki/Personal_data" rel="noopener noreferrer"&gt;Personally Identifiable Information&lt;/a&gt; (PII). Here are some important points to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User IDs should be opaque identifiers (like GUIDs) that don't reveal personal information&lt;/li&gt;
&lt;li&gt;Avoid logging email addresses, names, or other personal data&lt;/li&gt;
&lt;li&gt;Make sure your logging configuration doesn't send PII to systems where it shouldn't go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to comply with data protection regulations, consider implementing log retention policies and the ability to purge user data from logs when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expanding Context Enrichment
&lt;/h2&gt;

&lt;p&gt;User IDs are just the beginning. We can add more context to make our logs and traces even more useful:&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature Flags
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/blog/feature-flags-in-dotnet-and-how-i-use-them-for-ab-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;Feature flags&lt;/strong&gt;&lt;/a&gt; help us roll out new features gradually or enable them for specific users. Adding feature flag information to our context gives us valuable insights:&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="c1"&gt;// Inside the middleware&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;featureFlagService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NewFeature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;SetTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"features.newfeature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Add to logging scope as well&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tenant Information for Multi-tenant Applications
&lt;/h3&gt;

&lt;p&gt;If your application serves &lt;a href="https://www.milanjovanovic.tech/blog/multi-tenant-applications-with-ef-core" rel="noopener noreferrer"&gt;&lt;strong&gt;multiple tenants&lt;/strong&gt;&lt;/a&gt;, adding tenant IDs is extremely helpful:&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;tenantId&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;User&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;FindFirstValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TenantId"&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;tenantId&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&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="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;SetTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant.id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Add to logging scope&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Adding user context to your request tracing in ASP.NET Core is a simple but powerful technique. By implementing the middleware we've explored, you'll see several important benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Faster troubleshooting - when users report issues, you can quickly find relevant logs&lt;/li&gt;
&lt;li&gt;Better understanding of usage patterns - see which features are being used and by whom&lt;/li&gt;
&lt;li&gt;Improved performance monitoring - identify slow requests for specific user segments&lt;/li&gt;
&lt;li&gt;More effective A/B testing - track metrics for users with different feature flags&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Understanding logging scopes and the &lt;code&gt;Activity&lt;/code&gt; class helps you get the most out of this technique. Logging scopes ensure your log entries contain consistent contextual information, while activities enable distributed tracing across service boundaries. Remember to be careful with PII and make sure your logging practices comply with relevant regulations.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;p&gt;And stay awesome!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S. Whenever you're ready, there are 3 ways I can help you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Pragmatic Clean Architecture:&lt;/strong&gt;&lt;/a&gt; Join 3,700+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&amp;amp;utm_medium=website&amp;amp;utm_campaign=cross-posting"&gt;&lt;strong&gt;Modular Monolith Architecture:&lt;/strong&gt;&lt;/a&gt; Join 1,600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.patreon.com/milanjovanovic" rel="noopener noreferrer"&gt;&lt;strong&gt;Patreon Community:&lt;/strong&gt;&lt;/a&gt; Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>dotnet</category>
      <category>aspnetcore</category>
      <category>tracing</category>
      <category>logging</category>
    </item>
  </channel>
</rss>
