<?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: Manohari Jayachandran</title>
    <description>The latest articles on DEV Community by Manohari Jayachandran (@manoharij).</description>
    <link>https://dev.to/manoharij</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3978389%2Fcfe78c8c-e935-4e13-80d4-fc953898678e.png</url>
      <title>DEV Community: Manohari Jayachandran</title>
      <link>https://dev.to/manoharij</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manoharij"/>
    <language>en</language>
    <item>
      <title>Razor Syntax Fundamentals: Rendering the Task Tracker</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:47:55 +0000</pubDate>
      <link>https://dev.to/manoharij/razor-syntax-fundamentals-rendering-the-task-tracker-34o7</link>
      <guid>https://dev.to/manoharij/razor-syntax-fundamentals-rendering-the-task-tracker-34o7</guid>
      <description>&lt;p&gt;Parts 1 and 2 covered WPF and MVVM, a desktop application built and then properly structured. Part 3 pivots to the web, genuinely new territory. Razor is the syntax Blazor, covered in Part 4, is built directly on top of, so this post covers Razor on its own first, deliberately, before any framework gets layered in, the same Task Tracker data from the previous two posts, now rendered as a simple, read-only web page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Razor Actually Is
&lt;/h2&gt;

&lt;p&gt;Razor is a syntax for mixing C# directly into HTML, letting a single file describe both the visual structure of a page and the logic that decides what actually gets rendered, no separate templating language to learn, since the logic portions are just genuine C#.&lt;/p&gt;

&lt;p&gt;Think of a mail-merge letter template. Most of the letter is fixed, ordinary text, but certain spots contain a placeholder, "Dear [Name]", that gets filled in with real data before the letter is actually printed. Razor works the same way: most of a .cshtml file is plain HTML, but specific spots marked with @ get replaced with real, computed values, or entire blocks of logic, before the page is actually sent to a browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The @ Symbol: How Razor Knows Where C# Starts
&lt;/h2&gt;

&lt;p&gt;Everything in a Razor file is treated as plain HTML by default. The @ symbol is the signal that switches into C# mode for whatever comes immediately after it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Task Tracker&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;You have @Model.Tasks.Count tasks.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Everything here is plain HTML, rendered exactly
     as written, EXCEPT @Model.Tasks.Count - that
     specific expression is evaluated in C#, and the
     RESULT (a number) is what actually appears in
     the rendered page --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Razor is genuinely smart about where C# ends and HTML resumes again, it doesn't require an explicit closing symbol for a simple expression like this one, since it can tell from the syntax itself where the C# expression naturally ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixing C# and HTML Directly: A Small First Example
&lt;/h2&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;completedCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;);&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;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;Tracker&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;@completedCount&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;@Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;CODE&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;computing&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;directly&lt;/span&gt; &lt;span class="n"&gt;rendering&lt;/span&gt;
     &lt;span class="n"&gt;anything&lt;/span&gt; &lt;span class="n"&gt;itself&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;variable&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="nf"&gt;creates&lt;/span&gt;
     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completedCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;USED&lt;/span&gt; &lt;span class="n"&gt;further&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;inside&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;simple&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt; &lt;span class="n"&gt;expression&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;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/foreach"&gt;@foreach&lt;/a&gt;: Rendering a List
&lt;/h2&gt;

&lt;p&gt;Razor supports genuine C# control structures directly inside HTML, a &lt;a class="mentioned-user" href="https://dev.to/foreach"&gt;@foreach&lt;/a&gt; loop generates one copy of whatever HTML sits inside it, once per item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    @foreach (var task in Model.Tasks)
    {
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;@task.Description&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    }
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- This is a REAL C# foreach loop - the exact same
     syntax used anywhere else in C#. The &amp;lt;li&amp;gt; element
     inside the loop body gets rendered once per task
     in Model.Tasks, with @task.Description substituted
     in each time --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/if"&gt;@if&lt;/a&gt;: Conditional Rendering
&lt;/h2&gt;

&lt;p&gt;The same applies to &lt;a class="mentioned-user" href="https://dev.to/if"&gt;@if&lt;/a&gt; and else, genuine C# conditional logic, deciding which HTML actually gets rendered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    @foreach (var task in Model.Tasks)
    {
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
            @if (task.IsCompleted)
            {
                &lt;span class="nt"&gt;&amp;lt;s&amp;gt;&lt;/span&gt;@task.Description&lt;span class="nt"&gt;&amp;lt;/s&amp;gt;&lt;/span&gt;
            }
            else
            {
                &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;@task.Description&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            }
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    }
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Completed tasks render inside a &amp;lt;s&amp;gt; tag
     (strikethrough) - matching visually what checking
     a box did in the WPF version from Parts 1 and 2.
     Incomplete tasks render in a plain &amp;lt;span&amp;gt; instead --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rendering the Complete Task Tracker as a Read-Only Page
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@model TaskTrackerViewModel

&lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Task Tracker&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

@{
    var completedCount = Model.Tasks.Count(t =&amp;gt; t.IsCompleted);
}

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;@completedCount of @Model.Tasks.Count tasks completed&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    @foreach (var task in Model.Tasks)
    {
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
            @if (task.IsCompleted)
            {
                &lt;span class="nt"&gt;&amp;lt;s&amp;gt;&lt;/span&gt;@task.Description&lt;span class="nt"&gt;&amp;lt;/s&amp;gt;&lt;/span&gt;
            }
            else
            {
                &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;@task.Description&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
            }
        &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    }
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;

@if (!Model.Tasks.Any())
{
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;&lt;/span&gt;No tasks yet.&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
}

&lt;span class="c"&gt;&amp;lt;!-- @model at the very top declares WHAT TYPE of data
     this specific Razor file expects to receive - here,
     a TaskTrackerViewModel containing a list of tasks.
     This is what makes Model.Tasks, used throughout the
     rest of the file, actually valid and known at
     compile time --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TaskTrackerViewModel.cs - the plain C# class this&lt;/span&gt;
&lt;span class="c1"&gt;// Razor file expects, supplied by whatever is rendering&lt;/span&gt;
&lt;span class="c1"&gt;// the page (a Razor Page or an MVC controller action)&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;TaskTrackerViewModel&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Tasks&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;h2&gt;
  
  
  What's Deliberately Not Here Yet
&lt;/h2&gt;

&lt;p&gt;This page is genuinely read-only, there's no way to add a task, check a box, or delete anything from it. That's intentional: Razor by itself describes how to render HTML from data, nothing more. The interactivity, a button that actually does something without a full page reload, a checkbox that actually updates state, is exactly what Blazor, covered in the next post, adds on top of this same Razor syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Razor mixes real C# directly into HTML using the @ symbol, there's no separate templating language, the logic portions are genuine, ordinary C#.&lt;/p&gt;

&lt;p&gt;A code block computes values without directly rendering anything; a simple expression renders a value directly at that exact spot.&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/foreach"&gt;@foreach&lt;/a&gt; and &lt;a class="mentioned-user" href="https://dev.to/if"&gt;@if&lt;/a&gt; are the exact same C# control structures used anywhere else, just usable directly inside HTML markup.&lt;/p&gt;

&lt;p&gt;@model at the top of a file declares the exact type of data that file expects, giving compile-time knowledge of what Model actually contains throughout the rest of the file.&lt;/p&gt;

&lt;p&gt;Razor on its own only describes how to render HTML from data, it has no interactivity by itself, which is exactly the gap Blazor fills next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Part 4 covers Blazor, taking this exact Razor syntax and adding real interactivity on top of it, turning the read-only Task Tracker page built here into one where adding, checking, and deleting tasks actually works, without a full page reload.&lt;/p&gt;

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

&lt;p&gt;Razor lets C# and HTML live in the same file, using @ as the signal for where C# begins, a code block for computing values, a simple expression for rendering one directly, and genuine control structures like &lt;a class="mentioned-user" href="https://dev.to/foreach"&gt;@foreach&lt;/a&gt; and &lt;a class="mentioned-user" href="https://dev.to/if"&gt;@if&lt;/a&gt; for loops and conditionals. The Task Tracker rendered here is intentionally read-only, since Razor by itself only describes how to turn data into HTML, it has no concept of interactivity on its own. That gap is exactly what the next post's subject, Blazor, exists to fill, building directly on top of everything covered in this one.&lt;/p&gt;




&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>razor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>MVVM Explained: Refactoring the Task Tracker, Properly</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Wed, 16 Sep 2026 15:17:51 +0000</pubDate>
      <link>https://dev.to/manoharij/mvvm-explained-refactoring-the-task-tracker-properly-1h7g</link>
      <guid>https://dev.to/manoharij/mvvm-explained-refactoring-the-task-tracker-properly-1h7g</guid>
      <description>&lt;p&gt;Part 1 built a working Task Tracker in plain WPF, and closed by naming three pain points left visible on purpose, business logic tangled directly with UI code, a manual refresh step that was easy to forget (and one place where it actually was forgotten), and zero ability to unit test the filtering logic without running the full application. This post refactors that exact same app into proper MVVM, fixing all three, one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MVVM Actually Is
&lt;/h2&gt;

&lt;p&gt;MVVM stands for Model-View-ViewModel, a pattern that splits an application into three distinct roles, each with exactly one job, so that UI code and business logic never end up tangled together the way they were in Part 1.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Model&lt;/strong&gt; is what a Task actually is, its data, Description and IsCompleted, with no UI knowledge whatsoever. The &lt;strong&gt;View&lt;/strong&gt; is the XAML, purely visual, it knows how to display things, and nothing about what a "task" means or how filtering works. The &lt;strong&gt;ViewModel&lt;/strong&gt; is the middleman, it holds the tasks, knows how filtering works, and exposes everything the View needs to bind to, but has no direct reference to any XAML control at all.&lt;/p&gt;

&lt;p&gt;Think of a restaurant. The Model is the food itself, the actual dish, made of actual ingredients. The View is the plate and table setting, how it's presented to the customer, purely visual. The ViewModel is the waiter, the one who actually knows what's in the kitchen, takes the order, and carries information back and forth between the kitchen and the table, without the plate ever needing to know how the dish was cooked, and without the kitchen ever needing to know which table it's going to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Model: Just Data, Nothing Else
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TaskItem.cs - unchanged in SHAPE from Part 1, but now&lt;/span&gt;
&lt;span class="c1"&gt;// implements INotifyPropertyChanged, covered next&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;TaskItem&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotifyPropertyChanged&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_description&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&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;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Description&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_description&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="n"&gt;_description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;OnPropertyChanged&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="kt"&gt;bool&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;get&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;set&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;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;OnPropertyChanged&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;event&lt;/span&gt; &lt;span class="n"&gt;PropertyChangedEventHandler&lt;/span&gt; &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnPropertyChanged&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;CallerMemberName&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;name&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="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;PropertyChangedEventArgs&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  INotifyPropertyChanged: How the UI Finds Out Something Changed
&lt;/h2&gt;

&lt;p&gt;In Part 1, updating the UI after a change required manually calling RefreshList(), a step that was easy to forget, and genuinely was forgotten in the checkbox handler. INotifyPropertyChanged fixes this at the source: whenever a property's value actually changes, it raises an event announcing exactly that. The UI, bound to that property, is listening for this event and updates itself automatically, nobody has to remember to tell it anything.&lt;/p&gt;

&lt;p&gt;Think of a doorbell versus needing to personally walk around telling every single person in a house that someone arrived. Ring the doorbell once, raise the event, anyone who cares, anyone bound or listening, hears it and reacts automatically, without you tracking down each person individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ViewModel: Where the Actual Logic Now Lives
&lt;/h2&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;TaskViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotifyPropertyChanged&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_allTasks&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="n"&gt;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_visibleTasks&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_newTaskDescription&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_currentFilter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"All"&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;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;VisibleTasks&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_visibleTasks&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;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_visibleTasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;OnPropertyChanged&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;NewTaskDescription&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_newTaskDescription&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="n"&gt;_newTaskDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;OnPropertyChanged&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="n"&gt;ICommand&lt;/span&gt; &lt;span class="n"&gt;AddTaskCommand&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt; &lt;span class="n"&gt;DeleteTaskCommand&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt; &lt;span class="n"&gt;ShowAllCommand&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt; &lt;span class="n"&gt;ShowActiveCommand&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ICommand&lt;/span&gt; &lt;span class="n"&gt;ShowCompletedCommand&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;TaskViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AddTaskCommand&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AddTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;DeleteTaskCommand&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;DeleteTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ShowAllCommand&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ApplyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"All"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;ShowActiveCommand&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ApplyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Active"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;ShowCompletedCommand&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ApplyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Completed"&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;AddTask&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="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;NewTaskDescription&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;task&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;TaskItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NewTaskDescription&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;false&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;.&lt;/span&gt;&lt;span class="n"&gt;PropertyChanged&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ApplyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_currentFilter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// ^ this ViewModel LISTENS to each task's own&lt;/span&gt;
            &lt;span class="c1"&gt;// PropertyChanged - so checking a box automatically&lt;/span&gt;
            &lt;span class="c1"&gt;// re-applies the current filter, fixing the EXACT&lt;/span&gt;
            &lt;span class="c1"&gt;// bug from Part 1 where checking a box didn't&lt;/span&gt;
            &lt;span class="c1"&gt;// update the Completed filter until a button was&lt;/span&gt;
            &lt;span class="c1"&gt;// clicked again&lt;/span&gt;

            &lt;span class="n"&gt;_allTasks&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;task&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;NewTaskDescription&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="nf"&gt;ApplyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_currentFilter&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;DeleteTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&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;);&lt;/span&gt;
        &lt;span class="nf"&gt;ApplyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_currentFilter&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;ApplyFilter&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;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_currentFilter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter&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;filtered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"Active"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&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="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;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s"&gt;"Completed"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&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;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsEnumerable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;VisibleTasks&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;ObservableCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;filtered&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;event&lt;/span&gt; &lt;span class="n"&gt;PropertyChangedEventHandler&lt;/span&gt; &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnPropertyChanged&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;CallerMemberName&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;name&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="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PropertyChanged&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&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;PropertyChangedEventArgs&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's genuinely absent: no TaskListBox, no TaskInput.Text, no reference to any XAML control anywhere in this entire class. Every single method here operates purely on data and logic, which is exactly what makes it unit-testable, covered further below.&lt;/p&gt;

&lt;h2&gt;
  
  
  ICommand: Replacing Click Event Handlers
&lt;/h2&gt;

&lt;p&gt;In Part 1, a button's Click="AddTask_Click" called a method directly in code-behind. In MVVM, a button binds to an ICommand property on the ViewModel instead, the View never calls a method directly at all.&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;// RelayCommand.cs - a small, reusable ICommand&lt;/span&gt;
&lt;span class="c1"&gt;// implementation (a common, standard helper class)&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;RelayCommand&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;_execute&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;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_canExecute&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;canExecute&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_execute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_canExecute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canExecute&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;bool&lt;/span&gt; &lt;span class="nf"&gt;CanExecute&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;parameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_canExecute&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;()&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Execute&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;parameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;_execute&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;event&lt;/span&gt; &lt;span class="n"&gt;EventHandler&lt;/span&gt; &lt;span class="n"&gt;CanExecuteChanged&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CommandManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequerySuggested&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;remove&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CommandManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequerySuggested&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A generic version for commands needing a parameter,&lt;/span&gt;
&lt;span class="c1"&gt;// like DeleteTaskCommand needing to know WHICH task&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;RelayCommand&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="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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Action&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;_execute&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;RelayCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&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;execute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_execute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execute&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;bool&lt;/span&gt; &lt;span class="nf"&gt;CanExecute&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;parameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&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;Execute&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;parameter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;_execute&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;parameter&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;event&lt;/span&gt; &lt;span class="n"&gt;EventHandler&lt;/span&gt; &lt;span class="n"&gt;CanExecuteChanged&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;
  
  
  The View: Now Purely Visual
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Window&lt;/span&gt; &lt;span class="na"&gt;x:Class=&lt;/span&gt;&lt;span class="s"&gt;"TaskTracker.MainWindow"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml/presentation"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns:x=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml"&lt;/span&gt;
        &lt;span class="na"&gt;Title=&lt;/span&gt;&lt;span class="s"&gt;"Task Tracker"&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"450"&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Window.DataContext&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;local:TaskViewModel&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Window.DataContext&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;DockPanel&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Top"&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"0,0,0,10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding NewTaskDescription, UpdateSourceTrigger=PropertyChanged}"&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"250"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"0,0,10,0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Add Task"&lt;/span&gt; &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding AddTaskCommand}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Bottom"&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt; &lt;span class="na"&gt;HorizontalAlignment=&lt;/span&gt;&lt;span class="s"&gt;"Center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"All"&lt;/span&gt; &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding ShowAllCommand}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Active"&lt;/span&gt; &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding ShowActiveCommand}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Completed"&lt;/span&gt; &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding ShowCompletedCommand}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;ListBox&lt;/span&gt; &lt;span class="na"&gt;ItemsSource=&lt;/span&gt;&lt;span class="s"&gt;"{Binding VisibleTasks}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;ListBox.ItemTemplate&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;DataTemplate&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;CheckBox&lt;/span&gt; &lt;span class="na"&gt;IsChecked=&lt;/span&gt;&lt;span class="s"&gt;"{Binding IsCompleted}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Description}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"10,0,0,0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Delete"&lt;/span&gt;
                                &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"{Binding DataContext.DeleteTaskCommand,
                                          RelativeSource={RelativeSource AncestorType=Window}}"&lt;/span&gt;
                                &lt;span class="na"&gt;CommandParameter=&lt;/span&gt;&lt;span class="s"&gt;"{Binding}"&lt;/span&gt;
                                &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"20,0,0,0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/DataTemplate&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/ListBox.ItemTemplate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ListBox&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/DockPanel&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Window&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: zero Click= or Checked= event handlers anywhere. Every interaction is a binding or a command. This XAML file has no paired logic in MainWindow.xaml.cs at all, the code-behind file is now essentially empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Pain Points From Part 1, Fixed Directly
&lt;/h2&gt;

&lt;p&gt;Pain point one was business logic tangled with UI code. This is fixed: AddTask, DeleteTask, and ApplyFilter live entirely in TaskViewModel, with zero references to any XAML control. The View contains no logic at all.&lt;/p&gt;

&lt;p&gt;Pain point two was the manual RefreshList() that had to be remembered. This is fixed: INotifyPropertyChanged means the UI updates itself the moment VisibleTasks changes, or the moment any individual TaskItem's IsCompleted changes. There's no refresh step to forget, because there's no refresh step at all, the exact checkbox bug from Part 1 is now structurally impossible, not just fixed by remembering harder next time.&lt;/p&gt;

&lt;p&gt;Pain point three was that the filtering logic couldn't be unit tested. This is fixed too, covered next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Payoff: Genuinely Unit-Testable Filtering Logic
&lt;/h2&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;TaskViewModelTests&lt;/span&gt;
&lt;span class="p"&gt;{&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;ApplyFilter_Active_ReturnsOnlyIncompleteTasks&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 - no WPF, no Window, no UI of any&lt;/span&gt;
        &lt;span class="c1"&gt;// kind involved anywhere in this test&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;viewModel&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;TaskViewModel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTaskDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Task A"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTaskCommand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&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="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTaskDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Task B"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTaskCommand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&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="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VisibleTasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&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="c1"&gt;// Act&lt;/span&gt;
        &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShowActiveCommand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&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;// Assert&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VisibleTasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Task B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VisibleTasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Description&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 is exactly the kind of test that was impossible in Part 1, filtering logic lived directly inside a Click handler, tightly coupled to TaskListBox. Here, it's plain C# logic on a plain class, tested the same way as everything covered in the earlier unit testing post on this blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Model, View, and ViewModel each have exactly one job, data, visual presentation, and the logic connecting them, and MVVM's real value is keeping those three jobs from ever tangling back together.&lt;/p&gt;

&lt;p&gt;INotifyPropertyChanged replaces manual refresh calls with an event the UI listens for automatically, the exact missed-refresh bug from Part 1 becomes structurally impossible, not just less likely.&lt;/p&gt;

&lt;p&gt;ICommand replaces Click event handlers with bindable properties on the ViewModel, meaning the View never directly calls a method, it only ever binds to something.&lt;/p&gt;

&lt;p&gt;The genuine payoff of this whole refactor is testability, the same filtering logic that required running the full WPF app and clicking buttons in Part 1 is now a plain, fast unit test with zero UI involved.&lt;/p&gt;

&lt;p&gt;MVVM isn't extra complexity for its own sake, every piece introduced here directly fixes a specific, named problem from the plain code-behind version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Part 3 shifts to the web, Razor syntax fundamentals, the templating language Blazor is built on top of, covered next before Blazor itself in Part 4.&lt;/p&gt;

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

&lt;p&gt;MVVM splits an app into a Model (plain data), a View (purely visual XAML), and a ViewModel (the logic connecting them, with no knowledge of any specific UI control), the restaurant analogy of dish, plate, and waiter. Refactoring the exact Task Tracker from Part 1 into this shape fixed every pain point named there directly: tangled logic separated cleanly, the missed-refresh bug eliminated structurally through INotifyPropertyChanged, and the filtering logic made genuinely unit-testable for the first time. Nothing here was added for its own sake, every piece of MVVM introduced in this post exists because a specific, real problem from the plain version needed exactly that fix.&lt;/p&gt;




&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>wpf</category>
      <category>mvvm</category>
    </item>
    <item>
      <title>WPF Fundamentals: Building a Task Tracker, the Plain Way</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Tue, 15 Sep 2026 18:06:55 +0000</pubDate>
      <link>https://dev.to/manoharij/wpf-fundamentals-building-a-task-tracker-the-plain-way-3jeb</link>
      <guid>https://dev.to/manoharij/wpf-fundamentals-building-a-task-tracker-the-plain-way-3jeb</guid>
      <description>&lt;p&gt;This is Part 1 of a five-part series covering WPF, MVVM, Razor, Blazor, and Bootstrap, each genuinely new territory, explained from the ground up. The example running through this post is a small Task Tracker desktop app, add a task, mark it complete, delete it, filter by status (All, Active, Completed). It's built here using plain, direct code-behind, deliberately without MVVM, the next post in the series takes this exact same app and refactors it into proper MVVM, once the specific problems with this version are visible firsthand rather than described abstractly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WPF Actually Is
&lt;/h2&gt;

&lt;p&gt;WPF (Windows Presentation Foundation) is a framework for building desktop applications with rich, flexible user interfaces, using C# for logic and a separate markup language, XAML, to describe the visual layout.&lt;/p&gt;

&lt;p&gt;Think of building a house. XAML is the architectural blueprint, it describes where the rooms are, how big the windows are, what the layout looks like. C# code-behind is the electrician and plumber, it makes things actually do something, a light switch that turns on a light, a button that actually adds a task, once the structure is in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  XAML: The Markup Language, Explained
&lt;/h2&gt;

&lt;p&gt;XAML (eXtensible Application Markup Language) is a declarative way to describe a user interface, what controls exist, how they're arranged, and their visual properties, separately from the C# code that gives them behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Window&lt;/span&gt; &lt;span class="na"&gt;x:Class=&lt;/span&gt;&lt;span class="s"&gt;"TaskTracker.MainWindow"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml/presentation"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns:x=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml"&lt;/span&gt;
        &lt;span class="na"&gt;Title=&lt;/span&gt;&lt;span class="s"&gt;"Task Tracker"&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"450"&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Grid&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"TaskInput"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Add Task"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"AddTask_Click"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Grid&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/Window&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- x:Name gives an element an identifier that C#
     code-behind can reference directly (TaskInput.Text,
     for example)
     Click="AddTask_Click" wires this button to a specific
     C# METHOD, written in the paired code-behind file --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How this compares to something more familiar: XAML plays a similar role to HTML, describing structure and content declaratively, but it's specifically for a desktop window, not a browser page, and its tags map to actual .NET classes, a Button in XAML corresponds directly to the System.Windows.Controls.Button class, rather than browser-rendered HTML elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows and Controls
&lt;/h2&gt;

&lt;p&gt;A Window is the actual application window itself, the outermost container. Everything else, buttons, text boxes, lists, are controls placed inside it. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TextBox&lt;/strong&gt; is a single-line text input, where a new task's description gets typed. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Button&lt;/strong&gt; is a clickable element, triggering a specific action when clicked, adding a task, deleting one. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ListBox&lt;/strong&gt; displays a scrollable list of items, shows every task currently in the tracker. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CheckBox&lt;/strong&gt; is a toggle, marks a specific task as complete or not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layout Panels: Three Different Philosophies
&lt;/h2&gt;

&lt;p&gt;A layout panel controls how its child elements are actually arranged on screen. WPF offers several, each with a genuinely different approach.&lt;/p&gt;

&lt;p&gt;StackPanel stacks children one after another, vertically by default or horizontally, the simplest layout, good for a straightforward list of elements in a row or column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"TaskInput"&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Add"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"AddTask_Click"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grid arranges children in rows and columns, like a table, giving precise control over exactly where each element sits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Grid&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Grid.RowDefinitions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;RowDefinition&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"Auto"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;RowDefinition&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Grid.RowDefinitions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Grid.Row=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ListBox&lt;/span&gt; &lt;span class="na"&gt;Grid.Row=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"TaskListBox"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Grid&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DockPanel has children that "dock" to a specific edge, top, bottom, left, right, with the last child typically filling whatever space remains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;DockPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ListBox&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Bottom"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/DockPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of StackPanel like stacking books on a shelf, one after another. Grid is like a spreadsheet, with precise rows and columns. DockPanel is like sticky notes pressed to the edges of a whiteboard, with whatever's left in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Handling in Code-Behind
&lt;/h2&gt;

&lt;p&gt;"Code-behind" refers to the C# file paired directly with a XAML file, MainWindow.xaml.cs, paired with MainWindow.xaml, this is where the actual logic lives, referencing controls from the XAML by their x: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;// MainWindow.xaml.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainWindow&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Window&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;MainWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;InitializeComponent&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;AddTask_Click&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&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="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;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;TaskListBox&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="n"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&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;// AddTask_Click is wired directly to the Button's&lt;/span&gt;
&lt;span class="c1"&gt;// Click event in the XAML (Click="AddTask_Click").&lt;/span&gt;
&lt;span class="c1"&gt;// This method reaches DIRECTLY into TaskListBox and&lt;/span&gt;
&lt;span class="c1"&gt;// TaskInput - the controls defined in the XAML file -&lt;/span&gt;
&lt;span class="c1"&gt;// by their x:Name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Building the Complete Task Tracker
&lt;/h2&gt;

&lt;p&gt;Here's the full XAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Window&lt;/span&gt; &lt;span class="na"&gt;x:Class=&lt;/span&gt;&lt;span class="s"&gt;"TaskTracker.MainWindow"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml/presentation"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns:x=&lt;/span&gt;&lt;span class="s"&gt;"http://schemas.microsoft.com/winfx/2006/xaml"&lt;/span&gt;
        &lt;span class="na"&gt;Title=&lt;/span&gt;&lt;span class="s"&gt;"Task Tracker"&lt;/span&gt; &lt;span class="na"&gt;Height=&lt;/span&gt;&lt;span class="s"&gt;"450"&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;DockPanel&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Top"&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"0,0,0,10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;TextBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"TaskInput"&lt;/span&gt; &lt;span class="na"&gt;Width=&lt;/span&gt;&lt;span class="s"&gt;"250"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"0,0,10,0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Add Task"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"AddTask_Click"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;DockPanel.Dock=&lt;/span&gt;&lt;span class="s"&gt;"Bottom"&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt; &lt;span class="na"&gt;HorizontalAlignment=&lt;/span&gt;&lt;span class="s"&gt;"Center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"All"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"ShowAll_Click"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Active"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"ShowActive_Click"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Completed"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"ShowCompleted_Click"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;ListBox&lt;/span&gt; &lt;span class="na"&gt;x:Name=&lt;/span&gt;&lt;span class="s"&gt;"TaskListBox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;ListBox.ItemTemplate&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;DataTemplate&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;StackPanel&lt;/span&gt; &lt;span class="na"&gt;Orientation=&lt;/span&gt;&lt;span class="s"&gt;"Horizontal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;CheckBox&lt;/span&gt; &lt;span class="na"&gt;IsChecked=&lt;/span&gt;&lt;span class="s"&gt;"{Binding IsCompleted}"&lt;/span&gt;
                                  &lt;span class="na"&gt;Checked=&lt;/span&gt;&lt;span class="s"&gt;"TaskCheckBox_Changed"&lt;/span&gt;
                                  &lt;span class="na"&gt;Unchecked=&lt;/span&gt;&lt;span class="s"&gt;"TaskCheckBox_Changed"&lt;/span&gt;
                                  &lt;span class="na"&gt;Tag=&lt;/span&gt;&lt;span class="s"&gt;"{Binding}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;TextBlock&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"{Binding Description}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"10,0,0,0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Content=&lt;/span&gt;&lt;span class="s"&gt;"Delete"&lt;/span&gt; &lt;span class="na"&gt;Click=&lt;/span&gt;&lt;span class="s"&gt;"DeleteTask_Click"&lt;/span&gt; &lt;span class="na"&gt;Tag=&lt;/span&gt;&lt;span class="s"&gt;"{Binding}"&lt;/span&gt; &lt;span class="na"&gt;Margin=&lt;/span&gt;&lt;span class="s"&gt;"20,0,0,0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/StackPanel&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/DataTemplate&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/ListBox.ItemTemplate&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ListBox&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/DockPanel&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Window&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the full code-behind:&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainWindow&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Window&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&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;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_allTasks&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="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MainWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;InitializeComponent&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;AddTask_Click&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&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="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;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&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;task&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;TaskItem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&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;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="n"&gt;_allTasks&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;task&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;TaskInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;RefreshList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_allTasks&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;DeleteTask_Click&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&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;button&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Button&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&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;);&lt;/span&gt;
        &lt;span class="nf"&gt;RefreshList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_allTasks&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;TaskCheckBox_Changed&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&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;checkBox&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CheckBox&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;checkBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&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;.&lt;/span&gt;&lt;span class="n"&gt;IsCompleted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsChecked&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ShowAll_Click&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RefreshList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_allTasks&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;ShowActive_Click&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RefreshList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&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="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;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&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;ShowCompleted_Click&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;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RoutedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;RefreshList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_allTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&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;IsCompleted&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&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;RefreshList&lt;/span&gt;&lt;span class="p"&gt;(&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;TaskItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TaskListBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemsSource&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="n"&gt;TaskListBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemsSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tasks&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;class&lt;/span&gt; &lt;span class="nc"&gt;TaskItem&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;Description&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&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;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;h2&gt;
  
  
  The Pain Points Worth Noticing (Setting Up the Next Post)
&lt;/h2&gt;

&lt;p&gt;This app genuinely works, but a few things are worth sitting with honestly, since they're exactly what the next post's MVVM refactor addresses directly.&lt;/p&gt;

&lt;p&gt;Every single interaction is handled by code directly reaching into UI controls, TaskListBox.ItemsSource, TaskInput.Text, TaskInput.Clear(), the "business logic," what a task is, how filtering works, is tangled together with UI manipulation code in the exact same methods.&lt;/p&gt;

&lt;p&gt;RefreshList() has to be called manually, by hand, after every single change, add a task, refresh manually; delete a task, refresh manually; check a box, forgot to refresh at all, the checkbox handler above doesn't call it, so the Completed filter won't reflect a just-checked box until you click a filter button again, a genuinely easy bug to introduce accidentally.&lt;/p&gt;

&lt;p&gt;There is no way to unit test any of this logic in isolation, testing whether filtering to Active works correctly requires actually running the full WPF application and clicking buttons, since the filtering logic lives directly inside a Click event handler, tightly coupled to the UI itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;XAML is a declarative markup language describing WPF UI structure, playing a similar role to HTML but mapping directly to .NET classes rather than browser elements.&lt;/p&gt;

&lt;p&gt;StackPanel, Grid, and DockPanel represent three genuinely different layout philosophies, stacking, table-like precision, and edge-docking, each suited to different UI shapes.&lt;/p&gt;

&lt;p&gt;Code-behind directly wires XAML events to C# methods via x:Name references, straightforward, and genuinely workable for a small app like this one.&lt;/p&gt;

&lt;p&gt;The specific pain in this version isn't that it's wrong, it's that business logic and UI manipulation code live in the exact same place, making the app harder to test and easier to introduce subtle bugs into, like the missed refresh on checkbox change, as it grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Part 2 of this series takes this exact Task Tracker and refactors it into proper MVVM, separating what a task is and how filtering works, the ViewModel, from how it's actually displayed, the View, fixing the manual-refresh problem and making the filtering logic genuinely unit-testable for the first time.&lt;/p&gt;

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

&lt;p&gt;WPF describes a desktop UI declaratively through XAML, with C# code-behind providing the actual behavior, referencing controls directly by name. Layout panels, StackPanel, Grid, DockPanel, offer different ways to arrange those controls on screen. The Task Tracker built here genuinely works, but every interaction reaches directly into UI controls from within event handlers, tangling business logic with UI manipulation, which is precisely the shape of problem MVVM, covered in the next post, exists to solve.&lt;/p&gt;




&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>wpf</category>
      <category>desktop</category>
    </item>
    <item>
      <title>GitHub Copilot for C# Developers: Setup, Techniques, Agents, and Honest Tradeoffs</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:57:44 +0000</pubDate>
      <link>https://dev.to/manoharij/github-copilot-for-c-developers-setup-techniques-agents-and-honest-tradeoffs-85b</link>
      <guid>https://dev.to/manoharij/github-copilot-for-c-developers-setup-techniques-agents-and-honest-tradeoffs-85b</guid>
      <description>&lt;p&gt;GitHub Copilot gets mentioned constantly and explained properly rarely, most people know "it suggests code as you type" and stop there. This post covers it from the ground up, using the Product CRUD API from earlier posts as the working example, since that domain is already familiar and lets the focus stay entirely on Copilot itself rather than a new codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitHub Copilot Actually Is
&lt;/h2&gt;

&lt;p&gt;Copilot is an AI pair programmer built into your editor, trained on a large body of public code, that suggests code based on the context of what you're currently writing. It is not one single feature, it's actually three genuinely different tools sharing one name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Inline Suggestions&lt;/em&gt;&lt;/strong&gt; show ghost text as you type, predicting the next line or few lines, press Tab to accept, Esc to dismiss. This is the original, most common form of Copilot. &lt;br&gt;
&lt;strong&gt;&lt;em&gt;Copilot Chat&lt;/em&gt;&lt;/strong&gt; is a separate side panel where you ask questions, request explanations of existing code, or ask it to generate something specific through conversation. &lt;br&gt;
&lt;strong&gt;&lt;em&gt;Agent Mode&lt;/em&gt;&lt;/strong&gt; is given a broader task rather than a single suggestion, and can plan, edit multiple files, and iterate somewhat autonomously toward completing that task, covered in depth further down.&lt;/p&gt;

&lt;p&gt;Think of autocomplete on a phone keyboard, but instead of predicting the next word, it predicts the next several lines of code, based not just on what you just typed, but on the surrounding context of your open files, function names, and comments.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting It Up: VS Code
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the Extensions panel (Ctrl+Shift+X)&lt;/li&gt;
&lt;li&gt;Search for "GitHub Copilot"&lt;/li&gt;
&lt;li&gt;Install both "GitHub Copilot" (inline suggestions)
and "GitHub Copilot Chat" (the chat panel)&lt;/li&gt;
&lt;li&gt;Sign in when prompted - this links to your GitHub
account, which needs an active Copilot subscription
(individual, business, or via an organization)&lt;/li&gt;
&lt;li&gt;A small Copilot icon appears in the status bar,
bottom-right - click it to confirm it's active&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once installed, ghost-text suggestions appear automatically as you type in any supported file type, including .cs files.  &lt;/p&gt;
&lt;h2&gt;
  
  
  Setting It Up: Visual Studio
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Extensions menu -&amp;gt; Manage Extensions&lt;/li&gt;
&lt;li&gt;Search "GitHub Copilot"&lt;/li&gt;
&lt;li&gt;Install, then restart Visual Studio when prompted&lt;/li&gt;
&lt;li&gt;Sign in via the account icon, top-right corner&lt;/li&gt;
&lt;li&gt;Copilot Chat opens as a dedicated tool window,
accessible via View -&amp;gt; GitHub Copilot Chat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Visual Studio's integration surfaces suggestions the same way - inline, as you type - with Chat available as a separate panel for conversational requests.&lt;/p&gt;
&lt;h2&gt;
  
  
  Techniques That Genuinely Improve Suggestion Quality
&lt;/h2&gt;

&lt;p&gt;Copilot's suggestions are only as good as the context it has to work with, specific techniques noticeably change what it suggests.&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;// Writing a clear comment BEFORE the code - this&lt;/span&gt;
&lt;span class="c1"&gt;// genuinely steers the next suggestion&lt;/span&gt;
&lt;span class="c1"&gt;// validate that price is greater than zero and&lt;/span&gt;
&lt;span class="c1"&gt;// stock quantity is non-negative&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Copilot's suggestion here is shaped directly&lt;/span&gt;
    &lt;span class="c1"&gt;// by the comment above - likely something close to:&lt;/span&gt;
    &lt;span class="k"&gt;return&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;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&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;StockQuantity&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Descriptive naming as a strong signal of intent&lt;/span&gt;
&lt;span class="c1"&gt;// A vague name gives Copilot little to work with:&lt;/span&gt;
&lt;span class="k"&gt;public&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DoThing&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;x&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;// A descriptive name gives it a genuine, specific&lt;/span&gt;
&lt;span class="c1"&gt;// signal of what the method should actually do:&lt;/span&gt;
&lt;span class="k"&gt;public&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReserveProductStockAsync&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;productId&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;// Copilot's suggested implementation for the second&lt;/span&gt;
&lt;span class="c1"&gt;// version is noticeably more likely to match intent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping related files open matters too. If ProductDto.cs, IProductService.cs, and ProductsController.cs are all open in tabs while writing a new method in ProductService.cs, Copilot's suggestions draw on the actual shape of those related types, property names, method signatures, rather than guessing at a generic, unrelated pattern.&lt;/p&gt;

&lt;p&gt;Copilot Chat also has slash commands worth knowing: &lt;br&gt;
/explain explains what a selected block of code actually does&lt;br&gt;
/fix suggests a fix for a selected block with an error&lt;br&gt;
/tests generates unit tests for a selected method &lt;br&gt;
genuinely useful as a starting point, covered further below under the honest cons section.&lt;/p&gt;
&lt;h2&gt;
  
  
  What "Agent Mode" Actually Means
&lt;/h2&gt;

&lt;p&gt;The terminology here is genuinely confusing right now, worth untangling directly. Inline suggestions and Chat both respond to something you've already written or asked, one piece at a time. Agent mode is given a broader, higher-level task, "implement pagination for the GetAll endpoint," and can plan the necessary steps, edit multiple files, run terminal commands, and iterate somewhat autonomously toward completing that task, checking in with you at key decision points rather than requiring line-by-line approval.&lt;/p&gt;

&lt;p&gt;Autocomplete's scope is the next few lines, and you approve every single suggestion, constantly. Chat's scope is one specific question or request per message, and you approve by reading the response and deciding whether to use it. Agent mode's scope is a whole task, potentially spanning several files, and you approve at a higher level, reviewing a batch of changes, or specific decision points, rather than every individual line.&lt;/p&gt;

&lt;p&gt;Why this distinction matters practically: agent mode represents a genuine shift in how much you're reviewing versus how much you're trusting the tool to get right on its own, which directly changes how carefully you need to review the eventual output.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Genuine Pros
&lt;/h2&gt;

&lt;p&gt;Speed on boilerplate is real. DTOs, basic CRUD scaffolding, repetitive patterns, the exact kind of code shown across the CRUD post's good version, Copilot genuinely speeds up writing the first draft of structurally repetitive code.&lt;/p&gt;

&lt;p&gt;A fast first draft helps when you already know what correct looks like. Writing the general shape of a method you understand well, then reviewing and adjusting it, is often faster than typing every character by hand.&lt;/p&gt;

&lt;p&gt;It can also be a learning aid, used deliberately. Seeing an alternative approach to something you were about to write anyway can surface a pattern or method you didn't know existed, worth treating as a suggestion to evaluate, not an answer to accept.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Genuine Cons, Including a Real, Concrete Example
&lt;/h2&gt;

&lt;p&gt;Copilot suggests code that statistically resembles patterns it has seen, including patterns that are simply wrong, if that's what surrounds it in context.&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;// A REAL kind of situation worth being aware of:&lt;/span&gt;
&lt;span class="c1"&gt;// if an older, unrelated file in the same project&lt;/span&gt;
&lt;span class="c1"&gt;// still contained the BAD version's pattern from the&lt;/span&gt;
&lt;span class="c1"&gt;// CRUD post -&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM Products WHERE Id = "&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;// - Copilot may suggest something structurally similar&lt;/span&gt;
&lt;span class="c1"&gt;// for a NEW method, simply because that pattern exists&lt;/span&gt;
&lt;span class="c1"&gt;// elsewhere in the codebase it has as context. It does&lt;/span&gt;
&lt;span class="c1"&gt;// not independently know this is a SQL injection&lt;/span&gt;
&lt;span class="c1"&gt;// vulnerability - it's reflecting the surrounding code,&lt;/span&gt;
&lt;span class="c1"&gt;// good or bad, without judgment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few other genuine cons, stated plainly. There's a real over-reliance risk, accepting suggestions without reading them carefully is an easy habit to fall into, especially for boilerplate that looks right at a glance. Code can be confidently wrong, a suggestion can be syntactically correct, run without error, and still be logically wrong, Copilot has no way to verify business logic correctness, only pattern plausibility. Generated tests can test the wrong thing, /tests can produce a test that passes while checking a condition that doesn't actually matter, or missing the specific failure case, like the ones covered in the unit testing post, that a human would think to add deliberately. And there are genuine licensing and originality questions, a debated topic, since suggested code is generated from patterns in public code, and questions about attribution and originality are worth being aware of, even without a settled, universal answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Scenario and Solving Strategy
&lt;/h2&gt;

&lt;p&gt;The problem: in a live coding interview, you're asked to implement a new endpoint with Copilot enabled and visible. You want to use it efficiently without appearing to not understand your own code, and without accidentally accepting a subtly incorrect suggestion under time pressure.&lt;/p&gt;

&lt;p&gt;The strategy, step by step: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Write the method signature and a clear, specific&lt;br&gt;
comment BEFORE accepting any suggestion - this alone&lt;br&gt;
demonstrates you're directing the tool, not being&lt;br&gt;
directed by it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Read every suggestion before accepting - out loud if&lt;br&gt;
the format allows it ("this checks for null, then&lt;br&gt;
queries by id... looks right, accepting that") - this&lt;br&gt;
turns an invisible mental step into an audible,&lt;br&gt;
visible one for an interviewer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a suggestion looks reasonable but you're not&lt;br&gt;
fully certain, say so directly: "I'd want to verify&lt;br&gt;
this handles the not-found case correctly" - genuine,&lt;br&gt;
visible skepticism reads as competence, not hesitation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use Copilot for the boilerplate parts specifically&lt;br&gt;
(the DTO shape, the basic CRUD structure) and slow&lt;br&gt;
down deliberately for the actual business logic -&lt;br&gt;
this shows judgment about WHEN the tool is&lt;br&gt;
appropriate, not just that it exists&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If a suggestion is wrong, say so and correct it&lt;br&gt;
visibly, rather than silently accepting and moving on -&lt;br&gt;
catching and fixing an incorrect suggestion in real&lt;br&gt;
time is a stronger signal than never seeing one at all&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Copilot is three genuinely different tools under one name, inline suggestions, Chat, and agent mode, each with a different scope of autonomy and a different amount of review it actually needs from you.&lt;/p&gt;

&lt;p&gt;Clear comments and descriptive naming aren't just good practice for human readers anymore, they're the direct input that shapes what Copilot suggests next.&lt;/p&gt;

&lt;p&gt;Agent mode represents a real shift from approving every line to reviewing a batch of changes, worth understanding deliberately rather than treating it as just a faster version of autocomplete.&lt;/p&gt;

&lt;p&gt;Copilot reflects the patterns already present in its context, including bad ones, it has no independent judgment about whether a pattern like string-concatenated SQL is actually safe.&lt;/p&gt;

&lt;p&gt;The genuine skill isn't generating code quickly, it's reading generated code critically enough to know whether it's actually correct, which is exactly the skill built across the CRUD, Repository Pattern, and Unit Testing posts already on this blog.&lt;/p&gt;

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

&lt;p&gt;GitHub Copilot is genuinely useful for exactly the kind of repetitive, structurally predictable code shown throughout the CRUD and Repository Pattern posts, but its suggestions are a reflection of surrounding patterns, not a judgment about correctness. Inline suggestions, Chat, and agent mode represent increasing levels of autonomy, each requiring a correspondingly different level of review. Comments and naming aren't just documentation anymore, they're the actual mechanism for getting better suggestions. And the core skill this whole blog has been building toward, understanding SOLID principles, writing testable code, knowing what a correct fix actually looks like, is precisely what makes Copilot's output genuinely useful instead of a fast way to introduce a bug you didn't write yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://www.techstackblog.com/post.html?slug=github-copilot-csharp-guide" rel="noopener noreferrer"&gt;TechStack Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>githubcopilot</category>
      <category>ai</category>
    </item>
    <item>
      <title>Unit Testing in C# with xUnit and Moq: Proving the Repository Pattern Actually Pays Off</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:37:48 +0000</pubDate>
      <link>https://dev.to/manoharij/unit-testing-in-c-with-xunit-and-moq-proving-the-repository-pattern-actually-pays-off-nc4</link>
      <guid>https://dev.to/manoharij/unit-testing-in-c-with-xunit-and-moq-proving-the-repository-pattern-actually-pays-off-nc4</guid>
      <description>&lt;p&gt;The Repository Pattern &lt;a href="https://dev.to/manoharij/solid-the-repository-pattern-and-dependency-injection-one-complete-example-every-principle-4e75"&gt;post &lt;/a&gt;said, more than once, that BookReservationService was "genuinely unit-testable with a mock," because it depends on IBookRepository, an abstraction, rather than a concrete class talking directly to a real database. That claim was never actually backed up with a real test. This post keeps that promise properly, using the exact same Library Book Reservation example, explaining every concept along the way rather than assuming familiarity with testing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Unit Testing Actually Solves
&lt;/h2&gt;

&lt;p&gt;BookReservationService depends on IBookRepository and INotificationSender. In production, those are backed by a real EF Core repository talking to a real SQL Server database, and a real email-sending service. If you wanted to test BookReservationService's actual logic, does it correctly reserve a book, does it correctly send a notification only when reservation succeeds, testing it against the real versions would mean a real database has to exist and be reachable, test data has to be manually set up and cleaned up afterward, and a real email might actually get sent every time the test runs. This is slow, fragile, and genuinely unpleasant to run repeatedly.&lt;/p&gt;

&lt;p&gt;Think of testing a car's dashboard warning lights by actually driving the car empty of oil until the engine seizes, versus using a diagnostic bench that can simulate "oil level: critically low" as an input and simply checking whether the warning light turns on. The bench doesn't need a real engine at all, it just needs to convincingly send the right signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Mock Actually Is
&lt;/h2&gt;

&lt;p&gt;A mock is a fake object that implements the same interface as a real dependency, but where you, the person writing the test, control exactly what it returns for any given input, without any real database, network call, or external system involved at all.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0tmqy2q6ij57vuf4moe4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0tmqy2q6ij57vuf4moe4.png" alt="Mock" width="800" height="471"&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="c1"&gt;// The REAL implementation - used in production&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;BookRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IBookRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... talks to a real LibraryDbContext, a real database&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A MOCK - used only in tests, created automatically&lt;/span&gt;
&lt;span class="c1"&gt;// by the Moq library, never hand-written by you&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// YOU tell the mock exactly what to return when a&lt;/span&gt;
&lt;span class="c1"&gt;// specific method is called with specific arguments&lt;/span&gt;
&lt;span class="n"&gt;mockRepository&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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;Book&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Book"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IsAvailable&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;// From BookReservationService's perspective, this mock&lt;/span&gt;
&lt;span class="c1"&gt;// IS a perfectly valid IBookRepository - it has no way&lt;/span&gt;
&lt;span class="c1"&gt;// to tell the difference between this and a real one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the exact payoff of Dependency Inversion from the earlier post: because BookReservationService's constructor only asks for the interface, a test can hand it a completely fake implementation, and the service has no way to know or care.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up an xUnit Test Project
&lt;/h2&gt;

&lt;p&gt;xUnit is the testing framework, the tool that actually discovers your test methods, runs them, and reports pass or fail. Tests live in a separate project from your main application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Creating the test project (terminal / VS Code)&lt;/span&gt;
dotnet new xunit &lt;span class="nt"&gt;-n&lt;/span&gt; LibrarySystem.Tests
&lt;span class="nb"&gt;cd &lt;/span&gt;LibrarySystem.Tests
dotnet add reference ../LibrarySystem/LibrarySystem.csproj
dotnet add package Moq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A minimal first test - [Fact] marks a method as&lt;/span&gt;
&lt;span class="c1"&gt;// a single, standalone test case&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;BookReservationServiceTests&lt;/span&gt;
&lt;span class="p"&gt;{&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;ExampleTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Running tests&lt;/span&gt;
dotnet &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[Fact] tells xUnit "this method is a test, run it." Assert.True(...), and its many siblings, Assert.Equal, Assert.False, Assert.Null, is how a test states what it actually expects to be true, if the assertion fails, the test fails, and xUnit reports exactly which one and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Arrange-Act-Assert Pattern
&lt;/h2&gt;

&lt;p&gt;Nearly every well-written unit test follows the same three-part shape, and naming it explicitly makes tests dramatically easier to read and write consistently.&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;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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync_ReturnsTrue_WhenBookIsAvailable&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 - set up the mocks and the object being tested&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;mockRepository&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ACT - call the ONE method actually being tested&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ASSERT - check the outcome matches what was expected&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;True&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of a science experiment's structure: set up the conditions (Arrange), run the actual experiment (Act), record and check the result against the hypothesis (Assert). Separating these three phases clearly is what makes a test readable at a glance, rather than a tangled block where setup, execution, and checking are all mixed together.&lt;/p&gt;

&lt;p&gt;Note the naming convention: MethodName_ExpectedBehavior_Condition. ReserveBookAsync_ReturnsTrue_WhenBookIsAvailable reads almost like a sentence, and immediately tells you what broke just from the test name in a failure report, without needing to open the test method itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify: Checking That Something Happened, Not Just What Was Returned
&lt;/h2&gt;

&lt;p&gt;Assert checks a return value. Verify, a Moq-specific feature, checks that a specific method on a mock was actually called, which matters when the thing you care about isn't a return value, but a side effect.&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;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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync_SendsNotification_WhenReservationSucceeds&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;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;mockRepository&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Assert - VERIFY that SendAsync was actually called,&lt;/span&gt;
    &lt;span class="c1"&gt;// with the correct recipient, EXACTLY once&lt;/span&gt;
    &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&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="s"&gt;"member@example.com"&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;IsAny&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="n"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Once&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// It.IsAny&amp;lt;string&amp;gt;() means "don't care about the exact&lt;/span&gt;
&lt;span class="c1"&gt;// message text, just confirm SendAsync was called with&lt;/span&gt;
&lt;span class="c1"&gt;// this specific recipient" - useful when the precise&lt;/span&gt;
&lt;span class="c1"&gt;// wording isn't what the test is actually checking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test would fail if BookReservationService's ReserveBookAsync method forgot to call the notifier at all, even though the reservation itself might still correctly return true. Assert alone wouldn't have caught that; Verify specifically checks that the interaction actually happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Failure Path: What Should NOT Happen
&lt;/h2&gt;

&lt;p&gt;A genuinely thorough test suite checks the failure case just as carefully as the success case, specifically confirming that something that shouldn't happen, doesn't.&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;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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync_ReturnsFalse_WhenBookIsNotAvailable&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 - the mock is configured to simulate&lt;/span&gt;
    &lt;span class="c1"&gt;// a book that's already reserved&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;mockRepository&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Assert - the reservation correctly failed&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;False&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="c1"&gt;// AND, just as importantly - confirm the notification&lt;/span&gt;
    &lt;span class="c1"&gt;// was NEVER sent, since there's nothing to notify about&lt;/span&gt;
    &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&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;It&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAny&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="n"&gt;It&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAny&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="n"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Never&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 second assertion, Times.Never, is exactly the kind of check that catches a real bug: if someone later modifies BookReservationService and accidentally moves the notification call outside the if (success) block, this test fails immediately, loudly, and specifically, rather than the bug quietly reaching production where a member gets a confusing "reservation confirmed" email for a book they didn't actually get.&lt;/p&gt;

&lt;h2&gt;
  
  
  [Theory] and [InlineData]: One Test, Many Inputs
&lt;/h2&gt;

&lt;p&gt;Writing a nearly-identical test method for every input value you want to check is repetitive. [Theory] combined with [InlineData] runs the same test logic once per data row, keeping the test method itself written only once.&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;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&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="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"another@example.com"&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="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&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="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;ReserveBookAsync_ReturnsExpectedResult&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;bookId&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;bool&lt;/span&gt; &lt;span class="n"&gt;repositoryReturns&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;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;mockRepository&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookId&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="nf"&gt;ReturnsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repositoryReturns&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;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookId&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;// Assert&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repositoryReturns&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// xUnit runs this ONE method three times, once per&lt;/span&gt;
&lt;span class="c1"&gt;// [InlineData] row, reporting each as its own separate&lt;/span&gt;
&lt;span class="c1"&gt;// pass/fail result - not one combined test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of a single, reusable form with blank fields, filled out three different ways, rather than writing three entirely separate forms from scratch that happen to ask the same questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What NOT to Unit Test: The Distinction That Matters in an Interview
&lt;/h2&gt;

&lt;p&gt;BookReservationService is unit-tested, with mocks, exactly as shown above. The real BookRepository, the class that actually talks to EF Core and a real database, is not tested this same way. Testing that class against a real (or realistic, disposable) database is called an integration test, a genuinely different kind of test with a different purpose.&lt;/p&gt;

&lt;p&gt;A unit test tests one class's logic in isolation, every dependency is mocked, it's fast, milliseconds, no real database, no real network. Example: does BookReservationService call the notifier correctly when a reservation succeeds.&lt;/p&gt;

&lt;p&gt;An integration test confirms that multiple real pieces work together correctly, often using a real (or realistic, temporary) database, it's slower, but catches problems mocks physically cannot, a wrong SQL query, a broken EF Core mapping, a real connection string issue. Example: does BookRepository.ReserveAsync actually update the correct row in a real database.&lt;/p&gt;

&lt;p&gt;Why this distinction gets asked about directly: a common interview question is some version of "would you unit test your repository class?" The genuinely correct answer is no, not with mocks, because there's nothing left to fake once you're already testing the thing that talks to the real database. That class gets validated through an integration test instead, often using a real test database or an in-memory database provider specifically built for this purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete Test Class
&lt;/h2&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;BookReservationServiceTests&lt;/span&gt;
&lt;span class="p"&gt;{&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync_ReturnsTrue_WhenBookIsAvailable&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;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;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;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;True&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="p"&gt;}&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync_SendsNotification_WhenReservationSucceeds&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;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&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="s"&gt;"member@example.com"&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;IsAny&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="n"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Once&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="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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync_ReturnsFalse_WhenBookIsNotAvailable&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;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;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;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;False&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;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&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;It&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAny&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="n"&gt;It&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAny&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="n"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Never&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="n"&gt;Theory&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&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="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"another@example.com"&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="nf"&gt;InlineData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"member@example.com"&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="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;ReserveBookAsync_ReturnsExpectedResult&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;bookId&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;bool&lt;/span&gt; &lt;span class="n"&gt;repositoryReturns&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;mockRepository&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;mockRepository&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookId&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="nf"&gt;ReturnsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repositoryReturns&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;mockNotifier&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;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;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveBookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookId&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;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repositoryReturns&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="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;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;A mock is a fake implementation of an interface, entirely controlled by the test, it exists specifically because BookReservationService depends on IBookRepository, the abstraction, rather than a concrete class, which is the direct, practical payoff of Dependency Inversion.&lt;/p&gt;

&lt;p&gt;Arrange-Act-Assert is the standard shape of a readable unit test, set up, execute one action, check the result, and naming the sections explicitly, even just as comments, makes tests dramatically easier to follow.&lt;/p&gt;

&lt;p&gt;Assert checks a return value; Verify checks that a specific interaction actually happened, both matter, and testing only one of them can miss real bugs.&lt;/p&gt;

&lt;p&gt;Testing the failure path deliberately, Times.Never, Assert.False, is just as important as testing the happy path, it's often where the actual bugs hide.&lt;/p&gt;

&lt;p&gt;[Theory] and [InlineData] avoid duplicating nearly-identical test methods for different input values.&lt;/p&gt;

&lt;p&gt;Unit tests mock every dependency and test one class in isolation; integration tests use real, or realistic, dependencies to confirm multiple real pieces work together, knowing which kind of test fits which class is a genuinely common interview question.&lt;/p&gt;

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

&lt;p&gt;The Repository Pattern post claimed BookReservationService was "genuinely unit-testable" because it depends on an interface rather than a concrete class, this post is where that claim actually gets demonstrated, with real, runnable tests. A mock stands in for a real dependency, letting a test control exactly what happens without a real database anywhere in sight. Arrange-Act-Assert keeps each test readable. Verify catches missing side effects that a simple return-value check would miss entirely. And knowing the line between a unit test and an integration test, what gets mocked, what doesn't, and why, is exactly the kind of distinction that separates someone who has used a testing framework from someone who actually understands what each type of test is for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://www.techstackblog.com/post.html?slug=unit-testing-csharp-xunit-moq-explained" rel="noopener noreferrer"&gt;TechStack Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>unittesting</category>
      <category>testing</category>
    </item>
    <item>
      <title>Postman for API Work: Collections, Environment Variables, Secrets, Auth, and Tests</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Fri, 04 Sep 2026 23:08:24 +0000</pubDate>
      <link>https://dev.to/manoharij/postman-for-api-work-collections-environment-variables-secrets-auth-and-tests-23ob</link>
      <guid>https://dev.to/manoharij/postman-for-api-work-collections-environment-variables-secrets-auth-and-tests-23ob</guid>
      <description>&lt;p&gt;Postman is the tool most people use constantly and never actually learn properly, pasting URLs into a blank request, hardcoding a token directly into a header, clicking Send and eyeballing the response. This post covers it the way it's actually meant to be used, with the Product API from the earlier CRUD and Swagger posts as the working example throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collections and Requests
&lt;/h2&gt;

&lt;p&gt;A Collection is a named folder of related requests, keeping every endpoint for one API grouped together, instead of a scattered pile of unsaved tabs that disappear when Postman restarts.&lt;/p&gt;

&lt;p&gt;Think of a filing cabinet drawer labeled "Products API," with individual folders inside for each request type, versus loose papers scattered across a desk. Nothing is lost, nothing has to be rebuilt from memory the next time you need it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Products API (Collection)
  |- GET All Products
  |- GET Product By Id
  |- POST Create Product
  |- PUT Update Product
  |- DELETE Product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each request saves its method, URL, headers, body, and any tests written for it, permanently, reusable any time, by anyone the collection is shared with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Variables
&lt;/h2&gt;

&lt;p&gt;An Environment is a named set of variables, like baseUrl or apiKey, that requests reference using double curly brace syntax, instead of hardcoding literal values. Switching the active environment changes what every request in the collection actually points to, without editing a single request by hand.&lt;/p&gt;

&lt;p&gt;Think of a universal remote's device selector. The same physical buttons, play, pause, volume, work identically regardless of which device is currently selected, the remote just sends the command to whatever's active. An environment switch works the same way for your requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;Request&lt;/span&gt; &lt;span class="err"&gt;URL,&lt;/span&gt; &lt;span class="err"&gt;written&lt;/span&gt; &lt;span class="py"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="err"&gt;{{baseUrl}}/api/products&lt;/span&gt;

&lt;span class="err"&gt;Dev&lt;/span&gt; &lt;span class="py"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="py"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://localhost:5001&lt;/span&gt;

&lt;span class="err"&gt;Staging&lt;/span&gt; &lt;span class="py"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="py"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://staging-products-api.azurewebsites.net&lt;/span&gt;

&lt;span class="err"&gt;Production&lt;/span&gt; &lt;span class="py"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="py"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://products-api.azurewebsites.net&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching the dropdown at the top-right of Postman from Dev to Staging instantly redirects every request in the collection, GetAll, GetById, Create, Update, Delete, without touching any of them individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Secrets Actually Belong
&lt;/h2&gt;

&lt;p&gt;Never paste a real API key, token, or password directly into a request URL, header value, or body. Store it as a variable in an Environment instead, specifically marked as the secret type, not just a regular default variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Environment variables panel:

Variable          Type      Initial Value      Current Value
baseUrl           default   {{prod url}}       {{prod url}}
apiKey            secret    dots                dots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secret-type variables are masked in the UI, shown as dots, excluded from Postman's own sync and export in plain text where possible, and meaningfully reduce the risk of a real key ending up visible in a screen recording, a shared workspace, or an accidentally committed export file.&lt;/p&gt;

&lt;p&gt;Why this matters in practice: a request URL with a literal API key baked in gets saved into the collection permanently, anyone with access to that collection, a teammate, an accidental public share, now has that key too. A secret-type environment variable referenced by the apiKey variable keeps the actual value out of the request definition entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth Headers, Handled Properly, Not Hand-Built
&lt;/h2&gt;

&lt;p&gt;Postman's Authorization tab builds the correct header automatically for several common schemes, rather than requiring you to manually type the Authorization Bearer header into the Headers tab yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bearer Token&lt;/strong&gt;: paste the token, ideally as a variable, and Postman adds the Authorization Bearer header automatically. &lt;br&gt;
&lt;strong&gt;API Key&lt;/strong&gt;: choose header or query param, provide the key name and value, Postman places it correctly either way. &lt;br&gt;
&lt;strong&gt;Basic Auth&lt;/strong&gt;: username and password, Postman handles the Base64 encoding automatically, you never touch it directly. &lt;br&gt;
&lt;strong&gt;OAuth 2.0&lt;/strong&gt;: Postman can run the actual token-fetching flow, Authorization Code, Client Credentials, and so on, directly inside the tool, then automatically attach the resulting token to the request.&lt;/p&gt;

&lt;p&gt;Setting it once at the collection level: rather than configuring auth on every single request individually, set it once on the Collection itself, every request inside inherits it automatically, unless a specific request deliberately overrides it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building a Real Request
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="err"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;/api/products&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Headers:&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;Content-Type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application/json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(raw,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wireless Mouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;24.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stockQuantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This maps directly to the CreateProductDto from the CRUD post, Postman sends this JSON body, ASP.NET Core model-binds it into that exact DTO, and the Required and Range validation attributes from that post apply before the action method body even runs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Status Codes Worth Actually Checking
&lt;/h2&gt;

&lt;p&gt;Checking only "did this return 200" misses most of what actually matters about an API's behavior.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;200 OK is the default happy-path check, necessary but not sufficient on its own. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;404 Not Found tests whether GetById correctly returns this for a genuinely missing product, matching the fix from the CRUD post, NotFound() instead of Ok(null). &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;400 Bad Request tests whether Create correctly rejects an empty Name or a negative Price, confirming the validation attributes are actually being enforced. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;401 versus 403 are genuinely different meanings worth testing separately: 401 means you aren't authenticated at all, 403 means you are authenticated, but you don't have permission for this specific action. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;201 Created tests whether Create returns this specifically, not 200, with a Location header pointing to the new resource, matching the fix from the CRUD post. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;204 No Content tests whether Delete returns this specifically for a successful deletion with nothing further to return.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Tests Tab: Real Assertions, Not Eyeballing
&lt;/h2&gt;

&lt;p&gt;The Tests tab lets you write JavaScript assertions that run automatically every time the request is sent, checking the actual response against expected conditions, rather than a person visually scanning the JSON and hoping nothing looks wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Written in the Tests tab of the GetById request&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status code is 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response has expected fields&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Price is a positive number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;above&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// On the Create request specifically - testing the&lt;/span&gt;
&lt;span class="c1"&gt;// exact fix from the CRUD post&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create returns 201, not 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response includes a Location header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Location&lt;/span&gt;&lt;span class="dl"&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;Pre-request scripts set up state before the request fires. Postman also supports a Pre-request Script tab, running JavaScript before the request is sent, commonly used to generate a timestamp, compute a signature, or fetch a fresh token programmatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pre-request Script - saving a value from one response&lt;/span&gt;
&lt;span class="c1"&gt;// for use in a LATER request in the same collection&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Save the created product's id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdProductId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// The NEXT request (GetById) can now reference&lt;/span&gt;
&lt;span class="c1"&gt;// {{createdProductId}} directly, chaining requests&lt;/span&gt;
&lt;span class="c1"&gt;// together using real data from a previous step&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running a Whole Collection at Once
&lt;/h2&gt;

&lt;p&gt;The Collection Runner executes every request in a collection sequentially, running each request's tests automatically and producing a pass/fail summary, genuinely useful for confirming an entire API still behaves correctly after a change, not just one endpoint in isolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Collection Runner output (conceptual):

Products API - Run Summary
  GET All Products         PASS (2 tests)
  GET Product By Id        PASS (3 tests)
  POST Create Product      PASS (2 tests)
  PUT Update Product       PASS (1 test)
  DELETE Product           PASS (1 test)

  Total: 5 requests, 9 tests, 9 passed, 0 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This same collection can also run outside Postman's UI entirely, using Newman, Postman's command-line runner, making it possible to run this exact test suite inside a CI/CD pipeline, the same GitHub Actions workflow covered in an earlier post on this blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the Loop: Importing From the Swagger Post
&lt;/h2&gt;

&lt;p&gt;The earlier Swagger and OpenAPI post ended with exactly this workflow: with the app running, the OpenAPI spec is available at /swagger/v1/swagger.json. In Postman, choose Import and paste that URL, or upload the file, and Postman generates a complete collection automatically, every endpoint from ProductsController, correct verbs, correct routes, example bodies shaped from the DTOs. Everything covered in this post, environments, auth, tests, gets added on top of that auto-generated starting point, rather than building the whole collection by hand from nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Scenario and Solving Strategy
&lt;/h2&gt;

&lt;p&gt;The problem: a team manually tests the Products API by clicking through Postman requests one at a time before every deployment, eyeballing each response. A regression, Create returning 200 instead of 201, went unnoticed for two weeks because nobody happened to check that specific detail during manual testing.&lt;/p&gt;

&lt;p&gt;THE STRATEGY, STEP BY STEP:&amp;nbsp;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recognize this is a verification gap, not a testing effort gap - the team WAS testing, but manually, checking for "does it generally work" rather than specific, precise conditions&lt;/li&gt;
&lt;li&gt;Import the API directly from its Swagger/OpenAPI spec (covered above) to get a complete, accurate starting collection instead of one built and maintained by hand&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Add specific Tests tab assertions to every request - not just "status is 2xx" but the EXACT expected code (201 for Create, 204 for Delete, 404 for a missing GetById) - this is precisely the kind of narrow, easy-to-overlook detail manual eyeballing missed&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Use the Collection Runner to execute the entire suite in one pass before each deployment, producing a clear pass/fail summary instead of relying on someone remembering to manually check every endpoint&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Install Newman and add a step to the existing GitHub Actions CI/CD pipeline (covered in an earlier post) that runs this exact collection automatically on every push - turning "someone should really test this manually" into a check that happens whether anyone remembers to or not&amp;nbsp;&lt;/li&gt;
&lt;li&gt;The specific regression from the problem (201 vs 200) is now caught immediately, automatically, the next time it happens - rather than silently shipping for two weeks&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Environment variables let one collection work against dev, staging, and production without editing a single request, the variable changes, the requests don't.&lt;/p&gt;

&lt;p&gt;Secrets belong in secret-type environment variables, never hardcoded into a request directly, the difference matters the moment a collection gets shared or exported.&lt;/p&gt;

&lt;p&gt;The Authorization tab builds the correct header for Bearer, API Key, Basic Auth, and OAuth 2.0 automatically, and setting it once at the collection level avoids repeating it on every request.&lt;/p&gt;

&lt;p&gt;Checking only for a 200 misses most of what an API actually promises, 404, 400, 401 versus 403, 201, and 204 each represent a genuinely different, separately-testable behavior.&lt;/p&gt;

&lt;p&gt;The Tests tab turns "does this look right" into an actual, repeatable assertion, the same regression that manual eyeballing misses is exactly what a specific status-code test catches immediately.&lt;/p&gt;

&lt;p&gt;A Postman collection isn't just a manual tool, via Newman, the exact same tests can run inside a CI/CD pipeline, automatically, on every push.&lt;/p&gt;

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

&lt;p&gt;Postman's real value shows up once it moves past being a place to paste a URL and click Send. Collections keep requests organized and shareable, environment variables make the same collection work across every stage without editing anything, secret-type variables keep real credentials out of a shared file, the Authorization tab handles auth schemes correctly without manual header-building, and the Tests tab turns a visual check into a repeatable, automatable assertion. Combined with importing directly from a Swagger and OpenAPI spec and running the whole suite through Newman in CI/CD, Postman becomes a genuine verification tool, not just a way to manually poke at an API before hoping for the best.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on my blog: &lt;a href="https://www.techstackblog.com/post.html?slug=postman-api-testing-explained" rel="noopener noreferrer"&gt;TechStack Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>postman</category>
      <category>csharp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Swagger and OpenAPI: The Docs That Write Themselves</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Thu, 03 Sep 2026 14:29:13 +0000</pubDate>
      <link>https://dev.to/manoharij/swagger-and-openapi-the-docs-that-write-themselves-2hp4</link>
      <guid>https://dev.to/manoharij/swagger-and-openapi-the-docs-that-write-themselves-2hp4</guid>
      <description>&lt;p&gt;"Swagger" and "OpenAPI" get used interchangeably, and untangling that is actually the first useful thing to do here, one is a specification, the other is a tool that renders it. This post uses the Product API from an earlier CRUD post as the running example throughout, since the goal is showing exactly how documentation gets generated from code you've already written, not introducing a new domain to learn alongside a new concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Swagger and OpenAPI Actually Are
&lt;/h2&gt;

&lt;p&gt;OpenAPI is a specification, a structured JSON or YAML document that precisely describes an API: every endpoint, every parameter, every request body shape, every possible response. Swagger UI is one specific tool that takes that OpenAPI document and renders it as an interactive, browsable webpage, letting someone read the docs and actually try a request right from the browser. Swagger existed before OpenAPI became the open, vendor-neutral standard name for the specification format, which is exactly why people still say "Swagger" when they technically mean "OpenAPI."&lt;/p&gt;

&lt;p&gt;Think of a restaurant's recipe card versus its printed menu. The recipe card in the kitchen is the precise, structured truth, exact ingredients, exact quantities, exact steps. The printed menu a customer reads is a different, friendlier presentation of that same underlying information, letting the customer browse and choose without ever seeing the recipe card itself. OpenAPI is the recipe card. Swagger UI is the printed menu.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Actual Generated Document Looks Like
&lt;/h2&gt;

&lt;p&gt;Before going further, it helps to actually see one. This is a trimmed excerpt of the real JSON that AddSwaggerGen() would produce for the GetById endpoint used throughout this post, the file a browser fetches at /swagger/v1/swagger.json, and the exact same file Swagger UI reads to render its interactive page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Products API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"/api/Products/{id}"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Products"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"operationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetById"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int32"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#/components/schemas/ProductDto"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"404"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not Found"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ProductDto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int32"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"nullable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"double"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"stockQuantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int32"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every piece of this document maps directly to something already written in the code, "paths" comes from the controller's routes and HTTP verbs, the "200" and "404" entries come straight from the ProducesResponseType attributes covered next, and the ProductDto schema under "components" is a direct reflection of the DTO class itself, one property per line. This is genuinely the entire point of the post: nothing here was typed by hand into a documentation tool, it was all generated from code that already existed for other reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Documentation Actually Gets Generated
&lt;/h2&gt;

&lt;p&gt;The key thing to understand: you don't hand-write this specification. It's generated automatically by reflecting over your existing controllers, DTOs, and attributes, which is exactly why it can't silently drift out of date the way a manually maintained wiki page does.&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;// Program.cs - the two lines that turn this on&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="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;AddControllers&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;AddEndpointsApiExplorer&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;AddSwaggerGen&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="k"&gt;if&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;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&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;UseSwagger&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;UseSwaggerUI&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;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;span class="c1"&gt;// Visiting /swagger now shows a full interactive UI,&lt;/span&gt;
&lt;span class="c1"&gt;// built entirely from the ProductsController and DTOs&lt;/span&gt;
&lt;span class="c1"&gt;// already written for the CRUD post - nothing new to write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Attributes That Shape the Generated Docs
&lt;/h2&gt;

&lt;p&gt;By default, Swagger generates something reasonable but generic. Specific attributes let you control exactly what shows up, the response shapes, the possible status codes, and human-readable descriptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/[controller]"&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;ProductsController&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;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&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;ProductDto&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status200OK&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status404NotFound&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;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetById&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;id&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;product&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&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="n"&gt;product&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;These two ProducesResponseType attributes document both outcomes directly, a successful call returns a ProductDto shape with a 200, and a 404 is a documented, expected possibility, not a surprise. A frontend developer reading these generated docs never has to guess what a 404 from this specific endpoint means, or reverse-engineer it by triggering the error themselves.&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;// Adding a human-readable summary per endpoint&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;SwaggerOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Summary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Creates a new product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Validates the input and persists a new product record."&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;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;CreateProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&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;created&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;CreatedAtAction&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;GetById&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;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;created&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;created&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Requires the Swashbuckle.AspNetCore.Annotations NuGet package&lt;/span&gt;
&lt;span class="c1"&gt;// for [SwaggerOperation] specifically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Validation Attributes Show Up in the Docs Automatically
&lt;/h2&gt;

&lt;p&gt;Here's a nice callback to the CRUD post: CreateProductDto's validation attributes weren't just added there to reject bad requests server-side, they do double duty, since Swagger reads them too and documents the actual constraints 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;MaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&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;Name&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="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000000&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;Price&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="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&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;StockQuantity&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;// In the generated Swagger UI, the Name field now shows&lt;/span&gt;
&lt;span class="c1"&gt;// as required with a 200-character max, and Price shows&lt;/span&gt;
&lt;span class="c1"&gt;// its valid range - all from attributes that were ALREADY&lt;/span&gt;
&lt;span class="c1"&gt;// there for a completely different reason (server-side&lt;/span&gt;
&lt;span class="c1"&gt;// validation), now also serving as documentation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Documenting Security Schemes
&lt;/h2&gt;

&lt;p&gt;Without configuration, Swagger UI has no "Authorize" button and no way to actually test an authenticated endpoint from the browser. Adding a security scheme fixes 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;AddSwaggerGen&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="nf"&gt;AddSecurityDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&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;OpenApiSecurityScheme&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="s"&gt;"Authorization"&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;SecuritySchemeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Scheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BearerFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"JWT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ParameterLocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Enter a valid JWT token"&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;AddSecurityRequirement&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;OpenApiSecurityRequirement&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="n"&gt;OpenApiSecurityScheme&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Reference&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;OpenApiReference&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;ReferenceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SecurityScheme&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="s"&gt;"Bearer"&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="kt"&gt;string&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;span class="c1"&gt;// Swagger UI now shows an "Authorize" button - a caller&lt;/span&gt;
&lt;span class="c1"&gt;// can paste in a token once, and every subsequent&lt;/span&gt;
&lt;span class="c1"&gt;// "Try it out" request in the UI includes it automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Showing Multiple API Versions Side by Side
&lt;/h2&gt;

&lt;p&gt;The same versioning concept from the APIM Part 2 post applies directly here, Swagger can present multiple documented versions of an API, letting a caller see exactly what changed between v1 and v2.&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;AddSwaggerGen&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="nf"&gt;SwaggerDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v1"&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;OpenApiInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Products API"&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="s"&gt;"v1"&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;SwaggerDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v2"&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;OpenApiInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Products API"&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="s"&gt;"v2"&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;UseSwaggerUI&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="nf"&gt;SwaggerEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/swagger/v1/swagger.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Products API v1"&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;SwaggerEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/swagger/v2/swagger.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Products API v2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// A dropdown in Swagger UI now lets a caller switch between&lt;/span&gt;
&lt;span class="c1"&gt;// documented versions directly, rather than needing separate&lt;/span&gt;
&lt;span class="c1"&gt;// documentation pages maintained independently&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Practical Payoff: Exporting Into Postman
&lt;/h2&gt;

&lt;p&gt;A well-documented OpenAPI spec isn't just for humans reading a webpage, it's a structured document another tool can consume directly. Postman can import it and build a complete, ready-to-use collection automatically.&lt;/p&gt;

&lt;p&gt;With the app running, the raw spec is available at /swagger/v1/swagger.json. In Postman, choose Import and paste that URL, or upload the downloaded JSON file directly. Postman then generates a full collection automatically, every endpoint from ProductsController, correct HTTP verbs, correct routes, even example request bodies shaped according to CreateProductDto's properties.&lt;/p&gt;

&lt;p&gt;This is the direct bridge between writing good Swagger documentation and having a genuinely useful Postman collection, without manually building every request by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Scenario and Solving Strategy
&lt;/h2&gt;

&lt;p&gt;The problem: a frontend team keeps asking what fields the Create Product endpoint actually expects, what a 404 means specifically on GetById, and what the exact shape of a successful response looks like. Answers currently live in a wiki page that was accurate three months ago and hasn't been touched since.&lt;/p&gt;

&lt;p&gt;The strategy, step by step: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, recognize this is a documentation freshness problem, not a communication problem, the wiki isn't wrong because nobody cares, it's wrong because nothing keeps it in sync with the actual code. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, enable Swagger generation if it isn't already, AddSwaggerGen and UseSwaggerUI, since this alone produces accurate docs for every route, verb, and DTO shape, generated directly from the real, currently-running code. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third, add ProducesResponseType attributes to each action for every realistic outcome, 200, 404, 400, since this is what actually documents what a given status code means for this specific endpoint, not just that it's possible. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fourth, confirm CreateProductDto's existing validation attributes are already doing double duty as documentation, no extra work needed here, just point the frontend team at the generated docs instead of the stale wiki page. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, delete or clearly mark the old wiki page as deprecated, redirecting to the live Swagger UI, since the single source of truth is now the running application itself, not a separate document someone has to remember to update.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;OpenAPI is the specification, a structured document, while Swagger UI is one tool that renders it as a browsable page, the two terms get used interchangeably but describe different things.&lt;/p&gt;

&lt;p&gt;The documentation is generated from code that already exists, controllers, DTOs, attributes, which is exactly why it can't silently go stale the way a manually maintained wiki page does.&lt;/p&gt;

&lt;p&gt;ProducesResponseType documents both success and failure shapes explicitly, turning "this might return a 404" into a documented, expected part of the contract.&lt;/p&gt;

&lt;p&gt;Validation attributes on a DTO do double duty, the same Required and Range attributes enforcing rules server-side also document those exact constraints automatically.&lt;/p&gt;

&lt;p&gt;A security scheme definition is what actually makes the Authorize button in Swagger UI functional for testing authenticated endpoints directly in the browser.&lt;/p&gt;

&lt;p&gt;The OpenAPI spec is machine-readable, not just human-readable, Postman and other tools can import it directly to generate a complete, accurate collection automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The next post covers Postman itself in depth, collections, environment variables, where secrets actually belong, auth headers, request building, status codes, and writing tests, including the exact import workflow referenced at the end of this post.&lt;/p&gt;

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

&lt;p&gt;Swagger and OpenAPI solve a problem every API eventually has, documentation that's accurate the day it's written and wrong a month later. Generating the docs directly from the code, rather than writing them separately, is what actually keeps that from happening: a validation attribute, a response type, a security scheme, each one written once for a different practical reason, and each one automatically becoming part of the documentation too. The real payoff shows up at the boundary between tools, a well-documented API can hand its entire shape directly to something like Postman, turning "read the docs and manually build a request" into "import the spec and get a working collection immediately."&lt;/p&gt;




&lt;p&gt;Originally published at TechStack Blog:&lt;br&gt;
&lt;a href="https://www.techstackblog.com/post.html?slug=swagger-openapi-explained-with-example" rel="noopener noreferrer"&gt;https://www.techstackblog.com/post.html?slug=swagger-openapi-explained-with-example&lt;/a&gt; &lt;br&gt;
More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>SOLID, the Repository Pattern, and Dependency Injection: One Complete Example, Every Principle Explained in Place</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:30:19 +0000</pubDate>
      <link>https://dev.to/manoharij/solid-the-repository-pattern-and-dependency-injection-one-complete-example-every-principle-4e75</link>
      <guid>https://dev.to/manoharij/solid-the-repository-pattern-and-dependency-injection-one-complete-example-every-principle-4e75</guid>
      <description>&lt;p&gt;Two things get confused constantly: the Repository Pattern gets used without anyone explaining what it actually is, and Dependency Inversion (a design principle) gets treated as the same thing as Dependency Injection (a technique), when they're genuinely different ideas that happen to work together. This post explains both properly, then builds one complete example, a small library book reservation system, and walks through exactly where each of the five SOLID principles shows up in that one piece of code, with an analogy and a concrete "how it's achieved" for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Repository Pattern Actually Is
&lt;/h2&gt;

&lt;p&gt;A repository is a class whose only job is talking to a data source, a database, an API, a file, whatever it happens to be, and handing back plain objects. Everything else in the application, business logic, services, controllers, talks to the repository, never to the data source directly.&lt;/p&gt;

&lt;p&gt;Think of a librarian. You don't walk into the archive room and dig through the shelves yourself. You ask the librarian, "get me this book," and they know exactly where it is and how to retrieve it. You don't need to know how the archive is organized internally, alphabetically, by genre, by acquisition date, none of that is your concern. The repository is the librarian. Your business logic is the visitor asking for a book.&lt;/p&gt;

&lt;p&gt;How you actually achieve this: define an interface describing what can be asked for, get a book, save a book, mark a book reserved, then write one class that implements that interface using your real data source. Anything that needs data depends on the interface, never on the concrete data-access class directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIP vs DI: Two Different Things That Work Together
&lt;/h2&gt;

&lt;p&gt;The Dependency Inversion Principle, DIP, is a design rule. It says high-level code should depend on abstractions, interfaces, not on concrete, low-level classes. It's a statement about how your code should be shaped.&lt;/p&gt;

&lt;p&gt;Dependency Injection, DI, is the mechanism that actually makes DIP work in practice. Instead of a class creating its own dependency directly inside itself, something external hands that dependency to the class, usually through its constructor.&lt;/p&gt;

&lt;p&gt;Think of it this way. DIP is the rule that a lamp should plug into a standard wall socket, not have its wires hardwired directly into the wall. DI is the actual electrician who wires the socket and plugs the lamp in for you. DIP says what shape the connection should take. DI is who actually makes the connection happen. You can state the rule (DIP) without ever building the socket, and building the socket (DI) is what turns the rule into something real and usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Example: A Library Book Reservation System
&lt;/h2&gt;

&lt;p&gt;A small, complete system: a Book entity, a repository for storing and retrieving books, a notification piece that tells a member when their reserved book becomes available, and a service that ties the two together.&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;// Book.cs - the entity&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;Book&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;Id&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Author&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsAvailable&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="k"&gt;public&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;ReservedByMemberEmail&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IBookRepository.cs - the repository's contract&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;IBookRepository&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;Book&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&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;id&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllAvailableAsync&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReserveAsync&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;bookId&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;memberEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BookRepository.cs - the concrete implementation&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;BookRepository&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IBookRepository&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;LibraryDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&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;BookRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LibraryDbContext&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="n"&gt;_context&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="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;Book&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&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;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&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;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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllAvailableAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAvailable&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReserveAsync&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;bookId&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;memberEmail&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;book&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;Books&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookId&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;book&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="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAvailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&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;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAvailable&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;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReservedByMemberEmail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memberEmail&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="k"&gt;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// INotificationSender.cs - the notification contract&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;INotificationSender&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;SendAsync&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;recipient&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// EmailNotificationSender.cs - one implementation&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;EmailNotificationSender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotificationSender&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;SendAsync&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;recipient&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="c1"&gt;// real email-sending logic would go here&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;$"Email to &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recipient&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;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;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SmsNotificationSender.cs - a second implementation, added later&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;SmsNotificationSender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INotificationSender&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;SendAsync&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;recipient&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="c1"&gt;// real SMS-sending logic would go here&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;$"SMS to &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recipient&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;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;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BookReservationService.cs - the business logic, tying it together&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;BookReservationService&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;IBookRepository&lt;/span&gt; &lt;span class="n"&gt;_bookRepository&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;INotificationSender&lt;/span&gt; &lt;span class="n"&gt;_notificationSender&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;BookReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IBookRepository&lt;/span&gt; &lt;span class="n"&gt;bookRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;INotificationSender&lt;/span&gt; &lt;span class="n"&gt;notificationSender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_bookRepository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bookRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_notificationSender&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;notificationSender&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReserveBookAsync&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;bookId&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;memberEmail&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;success&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;_bookRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReserveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;memberEmail&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;success&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;_notificationSender&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;memberEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"Your book reservation is confirmed."&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;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs - registration&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;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LibraryDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LibraryDb"&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="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IBookRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BookRepository&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="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;INotificationSender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailNotificationSender&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="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;BookReservationService&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;That's the whole example. Every SOLID principle below points back into this same code, nothing new gets introduced from here on, only explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  S: Single Responsibility Principle
&lt;/h2&gt;

&lt;p&gt;What it means: a class should have exactly one reason to change.&lt;/p&gt;

&lt;p&gt;Think of a chef who only cooks, versus a chef who also has to answer phones, manage the register, and clean tables. The moment the chef is responsible for four unrelated things, a change to any one of them, a new phone system, a new register, risks disrupting the cooking too. One job per role means a change in one area never accidentally breaks another.&lt;/p&gt;

&lt;p&gt;Where it shows up here: BookRepository has exactly one job, talking to the database. BookReservationService has exactly one job, the actual reservation business rule, check availability, reserve, then notify. EmailNotificationSender has exactly one job, sending an email. None of these classes know or care about the others' internal details.&lt;/p&gt;

&lt;p&gt;How it's achieved: by drawing a boundary around each distinct concern, data access, business rules, notification delivery, and giving each its own class rather than letting one class quietly grow to handle all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  O: Open/Closed Principle
&lt;/h2&gt;

&lt;p&gt;What it means: a class should be open for extension but closed for modification. You should be able to add new behavior without editing code that already works and is already tested.&lt;/p&gt;

&lt;p&gt;Think of a power strip. Adding a new device means plugging into an open socket, not opening the strip and rewiring it. The strip itself never needs to change to support a new device.&lt;/p&gt;

&lt;p&gt;Where it shows up here: SmsNotificationSender was added as a brand new class, implementing the same INotificationSender interface EmailNotificationSender already implements. BookReservationService, which depends only on INotificationSender, needed zero changes to support this new notification method.&lt;/p&gt;

&lt;p&gt;How it's achieved: by depending on an interface, INotificationSender, rather than a specific class, so a new implementation can be introduced entirely alongside the old one, with the consuming code never needing to be touched or re-tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  L: Liskov Substitution Principle
&lt;/h2&gt;

&lt;p&gt;What it means: anywhere a base type or interface is expected, any implementation of it should be usable without breaking the correctness of the code using it.&lt;/p&gt;

&lt;p&gt;Think of a universal TV remote. Pressing power should turn the TV off, regardless of which TV brand is actually connected. If pressing power on one specific brand instead changed the channel, that brand would violate the substitutability the remote's design depends on.&lt;/p&gt;

&lt;p&gt;Where it shows up here: BookReservationService calls the notification sender's SendAsync method without knowing or caring whether it's actually an EmailNotificationSender or a SmsNotificationSender. Swapping the registration in Program.cs from one to the other requires zero changes to BookReservationService, and the reservation logic behaves correctly either way.&lt;/p&gt;

&lt;p&gt;How it's achieved: by making sure every implementation of INotificationSender genuinely fulfills the same contract, send this message to this recipient, without secretly requiring different treatment or producing different side effects the caller wasn't told to expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  I: Interface Segregation Principle
&lt;/h2&gt;

&lt;p&gt;What it means: no class should be forced to depend on methods it doesn't actually use. Prefer several small, focused interfaces over one large one that tries to do everything.&lt;/p&gt;

&lt;p&gt;Think of a restaurant menu split into starters, mains, and desserts, rather than one enormous single list. A customer who only wants dessert can look at exactly the dessert section, without wading through everything else to find what's relevant to them.&lt;/p&gt;

&lt;p&gt;Where it shows up here: INotificationSender has exactly one method, SendAsync. It was deliberately not designed as a bloated interface with unrelated methods bolted on, like getting notification history, scheduling reminders, or configuring retry policies. If those needs arise later, they belong in their own separate interface, so a simple SmsNotificationSender is never forced to implement functionality that has nothing to do with sending a message.&lt;/p&gt;

&lt;p&gt;How it's achieved: by keeping each interface scoped to one coherent capability, and introducing a new, separate interface when a genuinely different capability appears, rather than expanding an existing one to cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  D: Dependency Inversion Principle, and Where DI Actually Delivers It
&lt;/h2&gt;

&lt;p&gt;What it means, again: BookReservationService, the high-level class containing the actual business rule, depends on IBookRepository and INotificationSender, the abstractions, not on BookRepository or EmailNotificationSender, the concrete low-level classes.&lt;/p&gt;

&lt;p&gt;Where it shows up here: look at BookReservationService's constructor. Both parameters are interfaces. The class has no idea whether it's talking to a real SQL database or an in-memory test double, and no idea whether it's sending an email or a text message. That not-knowing is DIP, working correctly.&lt;/p&gt;

&lt;p&gt;How DI actually delivers this: the constructor doesn't create a new BookRepository or a new EmailNotificationSender anywhere inside itself. Something external has to hand BookReservationService its dependencies. That's exactly what the three lines in Program.cs do, registering each interface against a concrete implementation, so that when the DI container builds a BookReservationService, it automatically supplies a real BookRepository and a real EmailNotificationSender, without BookReservationService itself ever needing to know how those objects got constructed.&lt;/p&gt;

&lt;p&gt;This is the concrete answer to the DIP-versus-DI confusion from earlier: DIP is the fact that the constructor only asks for interfaces. DI is the three registration lines in Program.cs, plus the container's own internal machinery, that actually make those interfaces resolve to real objects at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Principle, At a Glance
&lt;/h2&gt;

&lt;p&gt;S, Single Responsibility: BookRepository only accesses data, BookReservationService only contains the reservation rule, each notification sender only sends one kind of message.&lt;/p&gt;

&lt;p&gt;O, Open/Closed: SmsNotificationSender was added as a new class, with zero changes to BookReservationService or EmailNotificationSender.&lt;/p&gt;

&lt;p&gt;L, Liskov Substitution: EmailNotificationSender and SmsNotificationSender are both fully substitutable wherever INotificationSender is expected, BookReservationService behaves correctly with either one.&lt;/p&gt;

&lt;p&gt;I, Interface Segregation: INotificationSender stays narrow, one method, one capability, rather than growing into an unrelated grab-bag interface.&lt;/p&gt;

&lt;p&gt;D, Dependency Inversion: BookReservationService's constructor depends only on IBookRepository and INotificationSender, never on a concrete class.&lt;/p&gt;

&lt;p&gt;DI, the mechanism: the three registration lines in Program.cs are what actually supply BookReservationService with real implementations at runtime, turning the DIP design rule into working code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;A repository's entire purpose is to be the one place that knows how to talk to the actual data source, so everything else in the application can depend on a simple, plain-object contract instead.&lt;/p&gt;

&lt;p&gt;DIP is a rule about how code should be shaped, depend on abstractions. DI is the mechanism, usually constructor injection plus a container, that actually fulfills that rule at runtime. They are not the same thing, even though they're almost always discussed together.&lt;/p&gt;

&lt;p&gt;Every SOLID principle in this example points at a specific, real design decision in the code, not an abstract definition floating on its own, Single Responsibility is visible in how the classes are split, Open/Closed is visible in how a new notification type was added, and so on through all five.&lt;/p&gt;

&lt;p&gt;A genuinely narrow interface, like INotificationSender with its one method, is what makes both Open/Closed and Interface Segregation possible at the same time, a bloated interface would have made adding SmsNotificationSender harder, not easier.&lt;/p&gt;

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

&lt;p&gt;The Repository Pattern is a data-access class hiding behind a simple interface, so business logic never needs to know how or where data actually lives. DIP is the design rule that high-level code should depend on that interface, not on a concrete implementation. DI is the actual mechanism, constructor injection plus a container, that supplies the real implementation at runtime, making DIP something that genuinely happens rather than something merely stated. Built together into one small library reservation example, all five SOLID principles stop being five separate definitions to memorize and become five specific, visible decisions inside one coherent piece of working code.&lt;/p&gt;




&lt;p&gt;Originally published at TechStack Blog: &lt;a href="https://www.techstackblog.com/post.html?slug=solid-repository-pattern-dependency-injection-library-example" rel="noopener noreferrer"&gt;https://www.techstackblog.com/post.html?slug=solid-repository-pattern-dependency-injection-library-example&lt;/a&gt;&lt;br&gt;
More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>solid</category>
      <category>architecture</category>
    </item>
    <item>
      <title>C# API CRUD Done Wrong, Then Done Right: A Complete Bad-Practices-vs-Best-Practices Walkthrough</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Sat, 29 Aug 2026 14:33:06 +0000</pubDate>
      <link>https://dev.to/manoharij/c-api-crud-done-wrong-then-done-right-a-complete-bad-practices-vs-best-practices-walkthrough-4fkk</link>
      <guid>https://dev.to/manoharij/c-api-crud-done-wrong-then-done-right-a-complete-bad-practices-vs-best-practices-walkthrough-4fkk</guid>
      <description>&lt;p&gt;Most bad-code examples in tutorials are contrived - obviously wrong in a way real code never actually looks. This post takes a different approach: a complete, realistic Product CRUD API, built with thirteen anti-patterns that genuinely show up in codebases. Each endpoint is walked through one at a time - the bad version, exactly what's wrong with it and why, then the good version, right next to each other, so nothing needs to be hunted down elsewhere on the page.&lt;/p&gt;

&lt;p&gt;The scope: a Product entity, five CRUD endpoints - GetAll, GetById, Create, Update, Delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Setup: Program.cs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad version:&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="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="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;AddControllers&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="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductsController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;span class="c1"&gt;// Inside ProductsController itself:&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="n"&gt;_connectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"Server=prod-sql-01;Database=Products;User Id=sa;Password=Passw0rd123!;"&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;Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hardcoded connection string. A literal password is committed directly into source code. Anyone with repository access, including, eventually, anyone the repository is ever accidentally exposed to, has the production database password in plain text. &lt;br&gt;
The fix: load the connection string from configuration, appsettings.json locally, environment variables or Key Vault in production, never as a literal string in a class.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incorrect service lifetime, AddSingleton for a controller. Manually registering the controller itself with AddSingleton means exactly one instance of ProductsController is created and shared across every single request for the entire lifetime of the application, rather than the framework creating a fresh instance per request the normal way. This becomes genuinely dangerous the moment that controller, or anything it depends on, holds any per-request state, and it gets worse once a Scoped dependency like a DbContext enters the picture, since DbContext is explicitly not thread-safe and is designed to live for exactly one request. A Singleton capturing a Scoped dependency is a well-known anti-pattern called a "captive dependency" - the Scoped service effectively gets trapped inside the Singleton's lifetime, silently becoming a de facto singleton itself, shared across requests and threads it was never designed to be shared across. &lt;br&gt;
The fix: never manually register a controller at all, AddControllers() already handles controller instantiation correctly per request, and give each dependency the lifetime that actually matches how it's meant to be used, which for something wrapping a DbContext means Scoped.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Good version:&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="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="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;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProductsDb"&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="n"&gt;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IProductService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProductService&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;The connection string now comes from configuration. IProductService is registered as Scoped, not Singleton, one instance per HTTP request, matching the lifetime of the DbContext it wraps, and avoiding the captive dependency problem entirely. The controller itself is never manually registered at all; AddControllers() handles that correctly on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Setup: The Product Entity
&lt;/h2&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;Product&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;Id&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&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;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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;StockQuantity&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;InternalSupplierNotes&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;CostPrice&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;Identical in both versions - the entity itself isn't the problem. InternalSupplierNotes and CostPrice are genuinely internal fields, what the business actually pays a supplier, never meant for a customer-facing API to expose. What differs between versions is whether this exact class ever gets returned to a caller directly, covered in the GetById section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint 1: GetAll
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad version:&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;HttpGet&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;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetAll&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;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetAllProductsAsync&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="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="n"&gt;products&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;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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllProductsAsync&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;products&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="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;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;new&lt;/span&gt; &lt;span class="nf"&gt;SqlConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connectionString&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;OpenAsync&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;SqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT * FROM Products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&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;reader&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;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteReaderAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;while&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;products&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;Product&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&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;reader&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="m"&gt;1&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDecimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&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;products&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;Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Missing await, .Result blocks the thread. GetAllProductsAsync().Result synchronously blocks the calling thread while waiting for the async method to complete, defeating the entire purpose of async and await, since the thread sits idle instead of being released back to the pool. In certain synchronization contexts, classic ASP.NET, WPF, WinForms, this specific pattern can cause a genuine deadlock. &lt;br&gt;
The fix: use await all the way up the call chain, never call .Result or .Wait() on an async method.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Undisposed SqlCommand and SqlDataReader. Both command and reader implement IDisposable, but only connection gets a using statement here. SqlDataReader in particular keeps the underlying connection in a busy state until it's explicitly closed, so under real load, with many concurrent requests each doing this, connections return to the pool later than they need to, risking pool exhaustion faster than necessary. The outer using on connection happens to clean these up indirectly when it disposes, which is what keeps this specific method from actively breaking right now, but relying on that indirect cleanup rather than disposing each IDisposable explicitly is fragile, and won't hold up if the method returns early or the pattern changes later. &lt;br&gt;
The fix: wrap every IDisposable in its own using statement, not just the outermost one.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Good version:&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;HttpGet&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;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAll&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;products&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAllAsync&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="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside ProductService:&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllAsync&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="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;Products&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;p&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;ProductDto&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;p&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;Name&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;Name&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;p&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;StockQuantity&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;StockQuantity&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&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;Genuinely awaited from the controller all the way through the service call. EF Core's LINQ query is also parameterized automatically and projects directly to ProductDto - a preview of the DTO pattern the next endpoint introduces properly. There's also no manual connection, command, or reader to dispose at all here - EF Core's DbContext and its query execution manage the underlying ADO.NET resources internally, which is one more reason the disposal issue from the bad version simply can't occur in this version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint 2: GetById
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad version:&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="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&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;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetById&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;id&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;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;new&lt;/span&gt; &lt;span class="nf"&gt;SqlConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connectionString&lt;/span&gt;&lt;span class="p"&gt;);&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;Open&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;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM Products WHERE Id = "&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="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;SqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&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;connection&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;reader&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="nf"&gt;ExecuteReader&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Read&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;Ok&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="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&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;Product&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&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;reader&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="m"&gt;1&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDecimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL injection. The id parameter is concatenated directly into the query string. A request to /api/products/1;DROP TABLE Products-- executes exactly that. &lt;br&gt;
The fix: never build SQL by concatenating user input, use a parameterized query, or an ORM like EF Core that parameterizes automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ok(null) instead of NotFound(). When the product doesn't exist, this returns a 200 OK with an empty body. The caller can't distinguish "found nothing" from "succeeded, and the answer happens to be nothing," both look identical on the wire. &lt;br&gt;
The fix: return NotFound(), a genuine 404, so the status code itself carries the information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No DTO, full entity exposed. The returned product is the raw entity, including InternalSupplierNotes and CostPrice, fields that should never reach an API caller. &lt;br&gt;
The fix: map to a ProductDto containing only the fields meant to be public.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Good version:&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductDto&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;Id&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&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;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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;StockQuantity&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&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;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetById&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;id&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;product&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&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="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside ProductService:&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;ProductDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&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;id&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;product&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;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&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;// FindAsync uses the primary key safely - parameterized&lt;/span&gt;
    &lt;span class="c1"&gt;// automatically, no string concatenation anywhere&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;product&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;return&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;// Returning null here is fine - it's the CONTROLLER's&lt;/span&gt;
    &lt;span class="c1"&gt;// job to translate this into the correct HTTP response,&lt;/span&gt;
    &lt;span class="c1"&gt;// not the service's&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProductDto&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;product&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;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&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;Price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&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;StockQuantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StockQuantity&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;All three issues fixed together: FindAsync makes SQL injection structurally impossible, NotFound() makes the response honest, and ProductDto makes leaking internal fields impossible rather than something to remember not to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint 3: Create
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad version:&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;HttpPost&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;IActionResult&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;Product&lt;/span&gt; &lt;span class="n"&gt;product&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;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;new&lt;/span&gt; &lt;span class="nf"&gt;SqlConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connectionString&lt;/span&gt;&lt;span class="p"&gt;);&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;Open&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;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"INSERT INTO Products (Name, Price) "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                 &lt;span class="s"&gt;$"VALUES ('&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&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="s"&gt;', &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&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="s"&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;SqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&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;connection&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="nf"&gt;ExecuteNonQuery&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;"Product created by user {Email} with auth token {Token}: {Name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;currentUser&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;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No validation. A request with an empty Name, a negative Price, or missing fields is accepted exactly as-is. &lt;br&gt;
The fix: add validation attributes to a dedicated input DTO, paired with ApiController's automatic model validation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL injection, again. Same pattern as GetById, product.Name concatenated directly, and this version breaks entirely if the name contains a single quote. &lt;br&gt;
The fix: same as before, EF Core, parameterized automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong status code. Returns 200 OK for a successful creation. &lt;br&gt;
The fix: 201 Created, with a Location header pointing to the new resource, since 200 doesn't communicate "a new thing was created."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive data logged in plain text. The user's email is reasonable to log, but the auth token is a genuine secret, and it's now sitting in plain text inside Application Insights or Log Analytics, visible to anyone with read access to logs, often retained for weeks or months. A leaked or overly-permissioned log query becomes a credential leak. &lt;br&gt;
The fix: never log secrets, tokens, passwords, or other sensitive fields, log an identifier (a user ID) instead of the credential itself, and if a field must be referenced for debugging, mask or redact it before it reaches the logger.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Good version:&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;MaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&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;Name&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="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000000&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;Price&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="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&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;StockQuantity&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;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;CreateProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No manual validation check needed - [ApiController]&lt;/span&gt;
    &lt;span class="c1"&gt;// automatically returns 400 Bad Request if the DTO's&lt;/span&gt;
    &lt;span class="c1"&gt;// validation attributes aren't satisfied, before this&lt;/span&gt;
    &lt;span class="c1"&gt;// method body even runs&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;created&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&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;_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;"Product {ProductId} created by user {UserId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;created&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;currentUser&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;// Logs an IDENTIFIER, never a credential or token -&lt;/span&gt;
    &lt;span class="c1"&gt;// enough to trace who did what, without exposing&lt;/span&gt;
    &lt;span class="c1"&gt;// anything an attacker or an over-permissioned log&lt;/span&gt;
    &lt;span class="c1"&gt;// reader could actually use&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;CreatedAtAction&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;GetById&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;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;created&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;created&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside ProductService:&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;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&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;product&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;Product&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;dto&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;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;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;StockQuantity&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;StockQuantity&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;Products&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;product&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProductDto&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;product&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;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&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;Price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&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;StockQuantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StockQuantity&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;CreateProductDto's Required, MaxLength, and Range attributes are checked automatically by ApiController. CreatedAtAction returns the correct 201 with a Location header built from the GetById action. The log statement now records a user ID, not a credential, and EF Core removes the SQL injection path entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint 4: Update
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad version:&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="nf"&gt;HttpPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&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;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Update&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;product&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="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;new&lt;/span&gt; &lt;span class="nf"&gt;SqlConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connectionString&lt;/span&gt;&lt;span class="p"&gt;);&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;Open&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;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"UPDATE Products SET Name = '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&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="s"&gt;', "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                    &lt;span class="s"&gt;$"Price = &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&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="s"&gt; WHERE Id = &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="s"&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;SqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&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;connection&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="nf"&gt;ExecuteNonQuery&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;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;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="nf"&gt;ToString&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="nf"&gt;Ok&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;Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Swallowed exception behavior masked as a response. The catch block doesn't silently discard the exception here, but it does something almost as problematic in a different way, it converts a failure into a 200 OK response body, meaning the HTTP status code itself claims success while the actual content says otherwise. Callers checking only the status code, which is the normal way to check for success, will treat this as a successful update. &lt;br&gt;
The fix: return an actual error status code, and don't rely on the response body alone to communicate failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL injection, again. Same string-concatenation pattern as the previous two endpoints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full exception details leaked to the caller. ex.ToString() returns the complete exception message and full stack trace, including internal file paths, method names, and sometimes literal fragments of the failed SQL query itself, all sent directly to whoever called this endpoint. This is genuinely useful reconnaissance for an attacker probing the API, and it's an accidental disclosure of internal implementation detail even to a well-meaning caller. &lt;br&gt;
The fix: log the full exception detail internally, server-side, and return only a generic, safe error message to the caller.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Good version:&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="nf"&gt;HttpPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&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;Update&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&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;success&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpdateAsync&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;dto&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;success&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;NotFound&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;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside ProductService:&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateAsync&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateProductDto&lt;/span&gt; &lt;span class="n"&gt;dto&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;product&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;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;return&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;product&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;dto&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;product&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;Price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StockQuantity&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;StockQuantity&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="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="k"&gt;return&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbUpdateException&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="c1"&gt;// The FULL exception, including stack trace, is&lt;/span&gt;
        &lt;span class="c1"&gt;// logged INTERNALLY here - visible to the team via&lt;/span&gt;
        &lt;span class="c1"&gt;// Application Insights, never sent to the caller&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="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Failed to update product {Id}"&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;return&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exception is now caught narrowly, DbUpdateException, not the broad Exception base class, logged internally in full detail with _logger.LogError, and the method returns false so the controller responds with a correct NotFound or, in a fuller version, a generic 500 with a safe message, never the raw exception content itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Endpoint 5: Delete
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bad version:&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="nf"&gt;HttpDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&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;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Delete&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;id&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;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;new&lt;/span&gt; &lt;span class="nf"&gt;SqlConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_connectionString&lt;/span&gt;&lt;span class="p"&gt;);&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;Open&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;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DELETE FROM Products WHERE Id = "&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="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;SqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&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;connection&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="nf"&gt;ExecuteNonQuery&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL injection, again. The same concatenation pattern as every other endpoint above.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong status code. Returns 200 OK with no body for a successful delete. The fix: 204 No Content, the correct response when an operation succeeds and there's genuinely nothing further to return.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Good version:&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="nf"&gt;HttpDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&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;Delete&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;id&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;success&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;_productService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeleteAsync&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;if&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&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;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside ProductService:&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DeleteAsync&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;id&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;product&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;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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;return&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;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&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="k"&gt;return&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;
  
  
  The Complete Good-Version Service Interface
&lt;/h2&gt;

&lt;p&gt;Pulling every service method from above together into the one interface the controller actually depends on:&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;IProductService&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetAllAsync&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;ProductDto&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&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;id&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;ProductDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateProductDto&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;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateAsync&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateProductDto&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;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DeleteAsync&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;id&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 is the layer that never existed in the bad version at all, the bad controller talked directly to SqlConnection. Having this interface is also what makes the controller genuinely unit-testable with a mock, something effectively impossible against the bad version's design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribute Reference: What's Doing the Actual Work
&lt;/h2&gt;

&lt;p&gt;ApiController enables automatic model validation, checking a DTO's validation attributes and returning 400 automatically if they fail, before the action method body runs. Present in both versions, but only doing real work in the good one, since the bad version's Product parameter has no validation attributes for it to check.&lt;/p&gt;

&lt;p&gt;Route with the controller token sets the base route, "api/products," identical in both versions.&lt;/p&gt;

&lt;p&gt;HttpGet, the id-scoped HttpGet, HttpPost, the id-scoped HttpPut, and the id-scoped HttpDelete map each method to its HTTP verb and route, the routing itself was never the problem in the bad version.&lt;/p&gt;

&lt;p&gt;Required, MaxLength, and Range on CreateProductDto are declarative validation rules, good version only, paired with ApiController to reject invalid requests automatically, with zero manual if-checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every Issue, At a Glance
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hardcoded connection string, in Program.cs and setup. &lt;br&gt;
Bad: literal password committed in source code. &lt;br&gt;
Good: loaded from configuration or Key Vault.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Incorrect service lifetime, AddSingleton for a controller, in Program.cs and setup. &lt;br&gt;
Bad: the controller manually registered as Singleton, one shared instance forever, risking a captive dependency once a Scoped DbContext enters the picture. &lt;br&gt;
Good: the controller is never manually registered at all, and IProductService is registered as Scoped, matching the DbContext's own lifetime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missing await / .Result, in GetAll. &lt;br&gt;
Bad: .Result blocks the thread synchronously. &lt;br&gt;
Good: await released the thread properly, all the way through.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Undisposed SqlCommand and SqlDataReader, in GetAll. &lt;br&gt;
Bad: only connection wrapped in a using, command and reader never explicitly disposed, relying on indirect cleanup that isn't reliable. &lt;br&gt;
Good: EF Core manages the underlying ADO.NET resources internally, so there's no manual disposal to get wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQL injection, appearing in GetById, Create, Update, and Delete. &lt;br&gt;
Bad: string-concatenated raw SQL. &lt;br&gt;
Good: EF Core LINQ or FindAsync, parameterized automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ok(null) instead of NotFound(), in GetById. &lt;br&gt;
Bad: 200 OK with a null body when not found. &lt;br&gt;
Good: NotFound(), a genuine 404.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No DTOs, full entity exposed, in GetById. &lt;br&gt;
Bad: internal fields like CostPrice and supplier notes exposed. &lt;br&gt;
Good: ProductDto excludes them entirely, structurally impossible to leak.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No validation, in Create. &lt;br&gt;
Bad: any data accepted as-is. &lt;br&gt;
Good: Required, MaxLength, and Range attributes plus ApiController auto-reject invalid requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wrong status codes, in Create and Delete. &lt;br&gt;
Bad: 200 OK for create and delete. &lt;br&gt;
Good: 201 Created with a Location header for create, 204 No Content for delete.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive data logged in plain text, in Create. &lt;br&gt;
Bad: an auth token logged directly, sitting in plain text in Application Insights. &lt;br&gt;
Good: only a user ID logged, never a credential or token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Swallowed exception behavior masked as a response, in Update. &lt;br&gt;
Bad: a failure converted into a 200 OK response body, status code claims success while content says otherwise. &lt;br&gt;
Good: an honest result returned, false propagated up so the controller can respond correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full exception details leaked to the caller, in Update. &lt;br&gt;
Bad: ex.ToString(), including the full stack trace, returned directly in the API response. &lt;br&gt;
Good: the full exception logged internally only, a generic safe message, if any, returned to the caller.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No service layer, structural, underneath all endpoints. &lt;br&gt;
Bad: controller does raw data access directly. &lt;br&gt;
Good: IProductService and ProductService, separated, testable, the natural home for every other fix.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Every anti-pattern here is realistic, these are mistakes that genuinely appear in real production code, not exaggerated examples built just to be wrong.&lt;/p&gt;

&lt;p&gt;The missing service layer is structurally connected to most of the other issues, a controller doing raw data access directly removes the natural place validation, parameterization, and error handling would otherwise live.&lt;/p&gt;

&lt;p&gt;A DTO isn't just extra code, it's what makes leaking internal fields structurally impossible, rather than something a developer has to remember not to do.&lt;/p&gt;

&lt;p&gt;Status codes are information, not decoration, Ok(null), 200 for a create, and 200 for a delete each communicate something genuinely false or ambiguous to the caller.&lt;/p&gt;

&lt;p&gt;Logging and error responses are both genuine attack surfaces, not just debugging conveniences, a logged token or a leaked stack trace can hand an attacker exactly what they need, even when the code otherwise "works."&lt;/p&gt;

&lt;p&gt;A swallowed exception, or one disguised as a success response, is worse than a crash, a crash is visible immediately, a silently hidden failure looks like success and can go unnoticed for a long time.&lt;/p&gt;

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

&lt;p&gt;Every issue in the bad version compiles, runs, and looks like reasonable code at a glance, which is exactly why these patterns survive in real codebases far longer than obviously broken code ever would. Rebuilding the same five endpoints with a real service layer, correct dependency lifetimes, disciplined resource disposal, DTOs, parameterized queries, genuine async, proper validation, correct status codes, and careful logging doesn't just fix thirteen individual bugs, it changes the shape of the code so that several of those bugs become structurally difficult to reintroduce by accident.&lt;/p&gt;




&lt;p&gt;Originally published at TechStack Blog: &lt;a href="https://www.techstackblog.com/post.html?slug=csharp-api-crud-bad-vs-good-practices" rel="noopener noreferrer"&gt;https://www.techstackblog.com/post.html?slug=csharp-api-crud-bad-vs-good-practices&lt;/a&gt; &lt;br&gt;
More from TechStack Blog: C# / .NET: &lt;a href="https://www.techstackblog.com/category.html?cat=csharp" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=csharp&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>webdev</category>
      <category>codereview</category>
    </item>
    <item>
      <title>Azure Integration Services Interview Prep Part 4: Application Insights, Log Analytics, Distributed Tracing, and Debugging a Broken Pipeline</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:37:15 +0000</pubDate>
      <link>https://dev.to/manoharij/azure-integration-services-interview-prep-part-4-application-insights-log-analytics-distributed-46kh</link>
      <guid>https://dev.to/manoharij/azure-integration-services-interview-prep-part-4-application-insights-log-analytics-distributed-46kh</guid>
      <description>&lt;p&gt;Part 1 covered the messaging services. Part 2 covered orchestration. Part 3 covered how the whole pipeline gets secured. None of that matters if something breaks at 2am and there's no way to find out why. This part covers the actual toolkit for debugging and monitoring a real Azure integration pipeline, and deliberately ties every tool back to the specific services already built across Parts 1 through 3, the same pattern the whole series has followed.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2sx5azr8z3904ahj7xig.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2sx5azr8z3904ahj7xig.png" alt="Part 4" width="800" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 1: Application Insights
&lt;/h2&gt;

&lt;p&gt;Application Insights is Azure's Application Performance Monitoring service, covered in real depth in an earlier post on this blog. It automatically collects telemetry from your applications - every request, every dependency call, every exception, every custom log message.&lt;/p&gt;

&lt;p&gt;Think of a flight data recorder, continuously capturing everything happening during a flight, automatically, without the pilot needing to manually log anything - so if something goes wrong, there's a complete, detailed record to investigate afterward.&lt;/p&gt;

&lt;p&gt;Application Insights fits anywhere in the pipeline that runs actual code - Function Apps, and any custom API behind APIM - anywhere telemetry needs to be automatically captured without manual logging discipline being the only safety net.&lt;/p&gt;

&lt;p&gt;To activate it in a Function App or any ASP.NET Core project, install the NuGet package &lt;code&gt;Microsoft.ApplicationInsights.AspNetCore&lt;/code&gt;, then in &lt;code&gt;Program.cs&lt;/code&gt;call &lt;code&gt;AddApplicationInsightsTelemetry&lt;/code&gt; with the connection string, linked either directly in App Service Configuration or via a Key Vault reference, which is the same Managed Identity plus Key Vault pattern covered in Part 3. Logic Apps get enabled differently, through Diagnostic Settings instead of code, since Logic Apps don't run custom compiled code - Diagnostic settings, Add diagnostic setting, Send to Log Analytics workspace, covered next.&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;// Function App / any ASP.NET Core project&lt;/span&gt;
&lt;span class="c1"&gt;// 1. Install NuGet: Microsoft.ApplicationInsights.AspNetCore&lt;/span&gt;
&lt;span class="c1"&gt;// 2. In Program.cs:&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;AddApplicationInsightsTelemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ApplicationInsights:ConnectionString"&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;What kind of debugging and monitoring it does&lt;/strong&gt;: it captures the four core telemetry types - &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
requests, every HTTP call in or out; &lt;/li&gt;
&lt;li&gt;dependencies, every outbound call your code makes, SQL, Service Bus, an external API; &lt;/li&gt;
&lt;li&gt;exceptions, full stack traces; &lt;/li&gt;
&lt;li&gt;traces, your own ILogger output. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the raw material every other debugging technique in this post is actually built on top of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: a Function App triggered by Service Bus, from Parts 1 and 2, automatically logs that trigger as a dependency call. A call to Key Vault for a secret, from Part 3, shows up as a dependency too. Nothing needs manual instrumentation for these - App Insights captures the entire pipeline's activity automatically, the moment it's wired in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 2: Log Analytics Workspace
&lt;/h2&gt;

&lt;p&gt;A Log Analytics workspace is the actual queryable data store that Application Insights telemetry, and diagnostic logs from many other Azure services, gets written into. This is a genuinely common point of confusion worth being precise about: Application Insights is the collector; Log Analytics is the warehouse the collected data actually lives in, queried using KQL.&lt;/p&gt;

&lt;p&gt;Think of it this way: if Application Insights is the flight data recorder capturing everything during the flight, Log Analytics is the secure archive facility where every recorder's data actually gets stored and can be pulled up for analysis later - and critically, multiple flight recorders, multiple Azure resources, can all feed into the same archive facility.&lt;/p&gt;

&lt;p&gt;This one-workspace, multiple-sources model matters because a single Log Analytics workspace can receive telemetry from Application Insights, your Function Apps' code-level telemetry, Logic Apps diagnostic logs covering run history and trigger or action outcomes, Service Bus diagnostic logs covering message counts and dead-letter activity, Azure SQL diagnostic logs covering query performance and errors, and APIM diagnostic logs covering every gateway request. This means one KQL query, against one workspace, can correlate data across services that would otherwise live in completely separate places.&lt;/p&gt;

&lt;p&gt;To activate it, first create a Log Analytics workspace, or use an existing one, through Azure Portal, Log Analytics workspaces, Create. Then for each service in the pipeline, point its diagnostic settings at this same workspace - Service Bus namespace, Diagnostic settings, Add, Send to Log Analytics workspace, and repeat this for Logic Apps, SQL, and APIM. Application Insights resources can also be workspace-based, meaning their data lands in this same shared Log Analytics workspace automatically rather than a separate silo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: it's not itself a monitoring feature - it's the foundation that makes cross-service KQL querying possible at all. Its actual monitoring value shows up entirely through the queries run against it, covered in the next topic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: a single Log Analytics workspace receiving diagnostic data from every service across the Part 2 end-to-end pipeline, APIM, Logic App, Function App, Service Bus, SQL, is what makes it possible to write one query tracing a single request across all of those services together, rather than needing to check five separate places manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 3: KQL Query Patterns for This Pipeline
&lt;/h2&gt;

&lt;p&gt;This is Kusto Query Language, covered in general depth in an earlier post on this blog, applied here specifically to the multi-service pipeline built across Parts 1 through 3, rather than a single application's logs in isolation.&lt;/p&gt;

&lt;p&gt;Think of it the same way it was covered in the earlier KQL post - SQL for your telemetry, reading left to right through pipe operators.&lt;/p&gt;

&lt;p&gt;KQL fits the Log Analytics workspace's Logs blade, or directly within Application Insights' own Logs view, which queries the same underlying data when workspace-based.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Failed&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="n"&gt;across&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;WHOLE&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;service&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;the&lt;/span&gt; &lt;span class="k"&gt;last&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;
&lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dependencies&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itemType&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;resultCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="n"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;dependency&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="n"&gt;specifically&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;general&lt;/span&gt; &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;
&lt;span class="n"&gt;dependencies&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;"Azure Service Bus"&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="nb"&gt;timestamp&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;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultCode&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Correlate&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;Function&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;specific&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="n"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;triggered&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;
&lt;span class="n"&gt;exceptions&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;operation_Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;"ProcessOrder"&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outerMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation_Id&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;operation_Id&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;covered&lt;/span&gt; &lt;span class="n"&gt;fully&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: ad-hoc investigation, answering a specific question about what happened, when, and how often, across however many services the query is scoped to touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: every problem scenario across Parts 1 and 2, a stuck order, a failed transformation, a dead-lettered message, ultimately gets investigated by writing a KQL query shaped like the ones above, filtered to the specific service, time window, and operation involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 4: Distributed Tracing with operation_Id
&lt;/h2&gt;

&lt;p&gt;Every request entering the pipeline gets a unique operation_Id automatically. Every dependency call, log message, and exception that happens while that request is being handled, across every service it touches, shares that same identifier.&lt;/p&gt;

&lt;p&gt;Think of a single tracking number on a package that gets handed off between multiple couriers, warehouses, and delivery trucks. Each handoff point logs an update against that same tracking number, so the package's entire journey can be reconstructed from pickup to delivery, regardless of how many different companies handled it along the way.&lt;/p&gt;

&lt;p&gt;This is automatic once Application Insights is wired in, from Topic 1, with no additional activation step needed. The operation_Id propagates automatically across HTTP calls, Service Bus messages, and Function App executions within the same Azure telemetry ecosystem. To make it explicitly visible in your own logs, you can log it directly alongside your own structured 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;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"Processing order {OrderId} with operation {OperationId}"&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;Id&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="n"&gt;RootId&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Tracing&lt;/span&gt; &lt;span class="n"&gt;ONE&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="s1"&gt;'s complete journey across
// every service it touched
union requests, dependencies, exceptions, traces
| where timestamp &amp;gt; ago(1h)
| where operation_Id == "abc123-def456"
| project timestamp, itemType, name, message, success
| order by timestamp asc

// Output reconstructs the ENTIRE path:
// 08:00:01 request   POST /orders (APIM)
// 08:00:02 dependency Logic App triggered
// 08:00:03 dependency Function App: ValidateOrder
// 08:00:04 dependency Service Bus: message sent
// 08:00:05 dependency Azure SQL: INSERT
// 08:00:05 request   200 OK returned
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: answers "what actually happened to this specific request" - the single most useful debugging technique when a specific customer or specific order is reported as broken, rather than a general pattern across many requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: this is the technique that makes the entire Part 2 complete-picture pipeline, APIM to Logic App to Function App to Service Bus to SQL, genuinely traceable end to end, exactly as referenced when that pipeline was first built - this topic is where that promise actually gets fulfilled with a real, runnable query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 5: Investigating a Failed Logic App Run
&lt;/h2&gt;

&lt;p&gt;Logic Apps maintain a Run History - a literal, step-by-step record of every trigger and action for every execution, including the exact input and output of each individual step.&lt;/p&gt;

&lt;p&gt;Think of a black-box flight recorder specifically for one single flight, showing every single instrument reading at every point during that one flight - not a general log, a complete replay of exactly what happened, action by action.&lt;/p&gt;

&lt;p&gt;To access it, go to your Logic App's Overview page in the Azure Portal, where Runs history is shown automatically, no separate activation needed, since this is built in by default. Click any run, especially one marked Failed, to see every action in the workflow, color-coded green for succeeded or red for failed. Click the red action specifically to see its exact input, what data it received, and exact output, the error it actually threw.&lt;/p&gt;

&lt;p&gt;The actual investigation flow looks like this: open Runs history and filter to Status Failed, click the specific failed run, visually scan for the red failed action in the sequence, click it and expand Inputs to see exactly what this action received from the previous step, expand Outputs to see the exact error message and status code returned, and use Resubmit to re-run this exact same run, with the same input, after fixing the underlying issue, without needing to wait for a new trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: deterministic, step-by-step replay of exactly what happened in one specific Logic App execution - genuinely the fastest way to debug a Logic App failure, since it requires no KQL query at all, just clicking through the visual designer's own run history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: in the Part 2 Logic Apps problem scenario, polling a partner API, validating, writing to SQL, a failure here, the partner API returning an unexpected shape, a validation condition behaving unexpectedly, is diagnosed by opening exactly this Run History view and inspecting the specific action that failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 6: Investigating a Function App Failure
&lt;/h2&gt;

&lt;p&gt;There are two complementary tools here - Live Metrics for what's happening right now, and Application Insights' Failures blade for investigating what already happened, down to the actual stack trace.&lt;/p&gt;

&lt;p&gt;Think of Live Metrics as watching a patient's vital signs monitor in real time during a procedure. The Failures blade is the detailed medical chart reviewed afterward to understand exactly what went wrong and when.&lt;/p&gt;

&lt;p&gt;Live Metrics, for real-time monitoring during an active incident, is accessed through your App Insights resource, Live Metrics, showing incoming request rate, failure rate, CPU and memory per instance, and exception rate, all with sub-second latency, genuinely real-time, with no activation needed beyond Application Insights already being wired in from Topic 1. The Failures blade, for after-the-fact investigation, is accessed through your App Insights resource, Failures, showing failed requests grouped by operation, exception types grouped and counted, and clicking into any one shows the full stack trace down to the exact line of code that threw.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;KQL&lt;/span&gt; &lt;span class="n"&gt;equivalent&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Failures&lt;/span&gt; &lt;span class="n"&gt;blade&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;shows&lt;/span&gt; &lt;span class="n"&gt;visually&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;querying&lt;/span&gt; &lt;span class="n"&gt;directly&lt;/span&gt;
&lt;span class="n"&gt;exceptions&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;operation_Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;"ProcessOrder"&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;summarize&lt;/span&gt; &lt;span class="k"&gt;Count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outerMessage&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;Count&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Groups&lt;/span&gt; &lt;span class="n"&gt;thousands&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;handful&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;same&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;technique&lt;/span&gt; &lt;span class="n"&gt;covered&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;earlier&lt;/span&gt; &lt;span class="n"&gt;KQL&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: Live Metrics answers "is this actively getting worse right now" during a live incident, particularly useful right after a deployment. The Failures blade answers "what specifically broke, how often, and where in the code" after the fact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: the Part 2 Function Apps problem scenario, complex order transformation logic, if that transformation throws an unexpected exception on a specific SKU format, the Failures blade's stack trace points to the exact line in the OrderTransformer class, from Part 2's example, where it happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 7: Dead-Letter Queue Investigation
&lt;/h2&gt;

&lt;p&gt;This is a direct callback to Service Bus from Part 1 - when a message exhausts its retry attempts, it moves to the Dead Letter Queue rather than disappearing, and it carries specific metadata explaining exactly why.&lt;/p&gt;

&lt;p&gt;Think of a returned-mail bin at a post office, where every piece of undeliverable mail gets a stamped note explaining specifically why it couldn't be delivered, refused, address not found, damaged, rather than just being discarded with no explanation.&lt;/p&gt;

&lt;p&gt;To access it through the portal, go to your Service Bus namespace, your queue, and look for the message count next to Dead-letter messages specifically, separate from the main queue's active message count. Programmatically, you create a receiver scoped to the dead-letter sub-queue 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;receiver&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="nf"&gt;CreateReceiver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"myqueue"&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;ServiceBusReceiverOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SubQueue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SubQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetter&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;messages&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;receiver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReceiveMessagesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&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;msg&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// These two fields are the actual answer to&lt;/span&gt;
    &lt;span class="c1"&gt;// "why did this fail" - always check them FIRST&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;$"Reason: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetterReason&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;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;$"Description: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetterErrorDescription&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;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;$"Body: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&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="s"&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 actual investigation decision, once you know why, breaks into three paths. A genuine data problem, a bad SKU format or a missing field, means fixing the source data and manually resubmitting the message. A transient issue now resolved, a downstream API briefly down during the original attempts, means simply resubmitting the message as-is, with no data change needed. An actual bug in the processing code means fixing the code, redeploying, and only then resubmitting affected messages, since resubmitting before the fix just dead-letters them again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: root-cause investigation for messages that definitively failed processing - the DeadLetterReason and DeadLetterErrorDescription fields specifically exist to turn "something failed" into a precise, actionable reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: this directly extends the Part 1 Service Bus problem scenario, per-customer ordered processing with a dead-letter queue configured - this topic is literally "what do you actually do once something lands there," which the earlier scenario's strategy stopped short of covering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 8: Alerting Strategy
&lt;/h2&gt;

&lt;p&gt;Alerting means proactive notification when something goes wrong, rather than only finding out when a customer complains or someone happens to check a dashboard.&lt;/p&gt;

&lt;p&gt;Think of a smoke detector versus manually checking every room for fire periodically. Alerting means the system tells you the moment something crosses a genuinely concerning threshold, rather than you needing to remember to go looking.&lt;/p&gt;

&lt;p&gt;To activate it, go to the specific resource, or the Log Analytics workspace for cross-service alerts, in the Azure Portal, then Alerts, Create, Alert rule. You define a signal or condition, either a metric threshold or a KQL log query returning results, an action group specifying who gets notified, email, SMS, a Teams webhook, or triggering another Function App, and a severity level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;genuinely&lt;/span&gt; &lt;span class="n"&gt;useful&lt;/span&gt; &lt;span class="n"&gt;custom&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fires&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt; &lt;span class="n"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;Dead&lt;/span&gt; &lt;span class="n"&gt;Letter&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;
&lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="k"&gt;search&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;AzureMetrics&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;MetricName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;"DeadletteredMessages"&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;DLQ&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt; &lt;span class="n"&gt;almost&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Topic&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;sitting&lt;/span&gt; &lt;span class="n"&gt;there&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;nobody&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;notified&lt;/span&gt; &lt;span class="n"&gt;defeats&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;purpose&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;having&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;DLQ&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;instead&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt; &lt;span class="n"&gt;losing&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;silently&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key distinction worth getting right is rate versus raw count. Alerting on "5 failures" means the same rule fires identically whether that's 5 failures out of 10 requests, a genuinely broken 50% failure rate, or 5 failures out of 50,000 requests, likely normal background noise. Alerting on failure rate, not raw count, is what actually distinguishes a real incident from expected noise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Rate&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;based&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;summarize&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;countif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;extend&lt;/span&gt; &lt;span class="n"&gt;FailureRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Failed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;FailureRate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Fires&lt;/span&gt; &lt;span class="k"&gt;only&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="k"&gt;MORE&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;last&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt; &lt;span class="nv"&gt;"5 failures happened"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What kind of debugging and monitoring it does&lt;/strong&gt;: it shifts from reactive, someone reported a problem, let's investigate, to proactive, the team knows within minutes, often before a customer notices at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 through 3&lt;/strong&gt;: a genuinely complete pipeline built across this series would have alerts on at minimum Service Bus DLQ count greater than 0, from Topic 7, Function App failure rate exceeding a threshold, from Topic 6, and Logic App run failure count, from Topic 5 - alerting is what turns everything else in this post from tools available if you go looking into the team finding out automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together: "Something Is Broken, Walk Me Through Your Process"
&lt;/h2&gt;

&lt;p&gt;This exact question shape is one of the most common in a panel interview for this topic area. Here's the structured answer, built entirely from this post's tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, an alert fires, from Topic 8 - failure rate on the order-processing Function App exceeded 10% in the last 5 minutes. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, open the Failures blade, from Topic 6, in Application Insights, and immediately see the exception type and count driving this, say, a specific ValidationException spiking. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third, click into one specific failed request and note its operation_Id. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fourth, run a distributed tracing query, from Topic 4, using that operation_Id, reconstructing the full path this specific request took: APIM, Logic App, Function App, and where it actually failed. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fifth, if the failure happened inside a Logic App action specifically, open Run History, from Topic 5, for that exact run, and inspect the failing action's input and output directly. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sixth, if the message was ultimately dead-lettered, from Topic 7, check DeadLetterReason and DeadLetterErrorDescription for the precise cause. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Seventh, once the root cause is identified, say a partner started sending a new SKU format the validation logic doesn't recognize, fix the code, redeploy, then resubmit the affected dead-lettered messages. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Eighth, write, or confirm the existence of, a KQL-based alert, from Topic 8, that would catch this specific failure pattern earlier next time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the shape of answer, naming specific tools in a specific order, each connected to a specific part of the pipeline already built across this series, that demonstrates real operational thinking, not just naming Application Insights as a one-word answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Application Insights collects telemetry automatically; Log Analytics is the queryable store that telemetry lives in - understanding this relationship matters more than most people initially assume.&lt;/p&gt;

&lt;p&gt;operation_Id is what makes a request genuinely traceable across every service it touches - this is the technique that fulfills the complete-picture promise from Part 2's pipeline diagram.&lt;/p&gt;

&lt;p&gt;A Logic App's Run History gives deterministic, step-by-step replay with exact input and output per action - often faster to debug than writing a KQL query at all.&lt;/p&gt;

&lt;p&gt;Live Metrics answers "what's happening right now"; the Failures blade answers "what already happened and exactly where in the code" - different tools for different moments in an incident.&lt;/p&gt;

&lt;p&gt;A dead-lettered message's DeadLetterReason and DeadLetterErrorDescription should always be checked first, before any other investigation - they often directly answer why without further digging.&lt;/p&gt;

&lt;p&gt;Alert on failure rate, not raw error count - the same absolute number means something completely different depending on total volume.&lt;/p&gt;

&lt;p&gt;The strongest interview answer to "something is broken, walk me through it" names specific tools in a specific sequence, each tied to a specific part of a real pipeline, not a generic list of Azure service names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Series, Complete
&lt;/h2&gt;

&lt;p&gt;This closes out the Azure integration interview prep series. Part 1 covered the messaging services - Service Bus, Storage Queues, Event Hub, Event Grid. Part 2 covered the orchestration layer - Logic Apps, Function Apps, Durable Functions. Part 3 covered how that pipeline actually gets secured - Managed Identity, Key Vault, VNet Integration, Private Endpoints, NSGs, RBAC, and token validation. Part 4 covered debugging and monitoring the pipeline once something inevitably goes wrong - Application Insights, Log Analytics, distributed tracing, and a structured process for the "something is broken" question. Four parts, one complete architecture story, from how data enters the system to how a failure gets traced back to its exact root cause.&lt;/p&gt;

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

&lt;p&gt;Debugging and monitoring complete the picture built across this entire series - a pipeline that moves data reliably, from Part 1, processes it with the right orchestration tool, from Part 2, and is genuinely secured end to end, from Part 3, still needs a real answer for "how do you know when it breaks, and how do you find out why." Application Insights and Log Analytics capture and store the telemetry. operation_Id makes any single request traceable across every hop. Run History, Live Metrics, and the Failures blade each answer a different shape of investigative question. Dead-letter queues carry their own answer if you know where to look. Alerting closes the loop, turning "someone eventually notices" into "the team knows within minutes." Together, these four parts describe a complete, real Azure integration architecture, which is exactly the shape of answer a panel interview is listening for.&lt;/p&gt;




&lt;p&gt;Originally published at TechStack Blog: &lt;a href="https://www.techstackblog.com/post.html?slug=azure-integration-interview-prep-part4" rel="noopener noreferrer"&gt;https://www.techstackblog.com/post.html?slug=azure-integration-interview-prep-part4&lt;/a&gt;&lt;br&gt;
More from TechStack Blog: Azure: &lt;a href="https://www.techstackblog.com/category.html?cat=azure" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=azure&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>interview</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Azure Integration Services Interview Prep Part 3: Managed Identity, Key Vault, VNet , Private Endpoints, NSGs, RBAC and Token Validation</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Sun, 23 Aug 2026 02:42:47 +0000</pubDate>
      <link>https://dev.to/manoharij/azure-integration-services-interview-prep-part-3-managed-identity-key-vault-vnet-private-5646</link>
      <guid>https://dev.to/manoharij/azure-integration-services-interview-prep-part-3-managed-identity-key-vault-vnet-private-5646</guid>
      <description>&lt;p&gt;Part 1 covered the messaging services - Service Bus, Storage Queues, Event Hub, Event Grid. Part 2 covered the orchestration layer that processes what those services move - Logic Apps, Function Apps, Durable Functions. Neither part addressed a question every panel interview eventually asks: how does any of this actually get secured. This part covers exactly that, and deliberately ties every concept back to the specific services already covered, rather than treating security as an abstract, separate topic.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0m3jj6ij00050o9niykw.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0m3jj6ij00050o9niykw.png" alt="Part 3" width="800" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 1: Managed Identity
&lt;/h2&gt;

&lt;p&gt;Managed Identity is a feature that gives an Azure resource, a Function App, a Logic App, a VM, its own automatically-managed identity in Azure AD, letting it authenticate to other Azure services without any developer ever storing, rotating, or managing a password, connection string secret, or certificate.&lt;/p&gt;

&lt;p&gt;Think of a hotel employee's own building access badge, versus needing to carry a physical master key that could be lost, copied, or stolen. The badge is tied to that specific employee's identity, can be individually revoked without affecting anyone else, and nobody needs to manage a shared physical key that everyone has to protect.&lt;/p&gt;

&lt;p&gt;There are two types of Managed Identity. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;System-assigned&lt;/strong&gt; is tied directly to one specific resource's lifecycle - created when the resource is created, deleted when the resource is deleted, and cannot be shared across multiple resources. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User-assigned&lt;/strong&gt; is created as its own standalone Azure resource, then assigned to one or more other resources, surviving independently of any single resource's lifecycle - useful when several Function Apps need to share the same identity and permission set.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Enabling system-assigned identity - Azure Portal:&lt;/span&gt;
&lt;span class="c1"&gt;// Function App -&amp;gt; Identity -&amp;gt; System assigned -&amp;gt; On -&amp;gt; Save&lt;/span&gt;

&lt;span class="c1"&gt;// Using it in C# - DefaultAzureCredential automatically&lt;/span&gt;
&lt;span class="c1"&gt;// discovers and uses the Managed Identity when running&lt;/span&gt;
&lt;span class="c1"&gt;// in Azure, no connection string, no client secret&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.Identity&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;Azure.Security.KeyVault.Secrets&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;client&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;SecretClient&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://your-vault.vault.azure.net/"&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;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;KeyVaultSecret&lt;/span&gt; &lt;span class="n"&gt;secret&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SqlConnectionString"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// No password anywhere in this code. The Function App's&lt;/span&gt;
&lt;span class="c1"&gt;// Managed Identity IS the credential.&lt;/span&gt;

&lt;span class="c1"&gt;// Granting access - Azure RBAC, not a stored key&lt;/span&gt;
&lt;span class="c1"&gt;// Key Vault -&amp;gt; Access control (IAM) -&amp;gt; Add role assignment&lt;/span&gt;
&lt;span class="c1"&gt;// Role: Key Vault Secrets User&lt;/span&gt;
&lt;span class="c1"&gt;// Assign to: the Function App's Managed Identity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: Managed Identity is genuinely close to strictly better than a stored secret for Azure-to-Azure authentication - the honest limitation is that it only works for authenticating to Azure services, or anything supporting Azure AD auth. A third-party, non-Azure API that only accepts an API key still needs that key stored somewhere, which is exactly where Key Vault comes in next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: a Function App with a Service Bus Trigger, authenticates to Service Bus, using Managed Identity rather than a stored Shared Access Signature connection string - the same pattern applies to that Function App writing to Azure SQL as an exit point, or a Logic App's Azure Function connector calling into a Function App. Every service-to-service hop in the pipelines built across Parts 1 and 2 is a candidate for Managed Identity instead of a stored credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 2: Azure Key Vault
&lt;/h2&gt;

&lt;p&gt;Azure Key Vault is a managed service for storing secrets, encryption keys, and certificates securely, covered in depth in an earlier post on this blog. Worth revisiting here specifically in relation to Managed Identity: Key Vault is where the secrets that genuinely can't be eliminated by Managed Identity still need to live.&lt;/p&gt;

&lt;p&gt;Think of a bank vault, with your application holding a key card, the Managed Identity, that lets it walk in and request a specific secret, rather than holding the secret itself.&lt;/p&gt;

&lt;p&gt;Managed Identity eliminates the need to store Azure SQL connection strings when using Azure AD auth, Service Bus connection strings when using RBAC and identity, and Storage account access keys when using RBAC and identity. What still needs to live in Key Vault includes a partner's external API key, since Salesforce or ServiceNow don't know what an Azure Managed Identity is, a third-party webhook signing secret, and any credential for a system outside Azure entirely.&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;// Key Vault reference in App Service Configuration -&lt;/span&gt;
&lt;span class="c1"&gt;// no SDK code needed for this pattern&lt;/span&gt;
&lt;span class="c1"&gt;// App Service -&amp;gt; Configuration -&amp;gt; Application settings&lt;/span&gt;
&lt;span class="c1"&gt;// Name: SalesforceApiKey&lt;/span&gt;
&lt;span class="c1"&gt;// Value: @Microsoft.KeyVault(SecretUri=https://your-vault.vault.azure.net/secrets/SalesforceApiKey/)&lt;/span&gt;

&lt;span class="c1"&gt;// Read exactly like any other configuration value&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"SalesforceApiKey"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// App Service resolves the Key Vault reference at&lt;/span&gt;
&lt;span class="c1"&gt;// startup automatically, using ITS OWN Managed Identity&lt;/span&gt;
&lt;span class="c1"&gt;// to authenticate to Key Vault - the two concepts&lt;/span&gt;
&lt;span class="c1"&gt;// working together directly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: Key Vault has real per-operation costs and API rate limits - it's for genuine secrets, not general configuration values like a page size or a feature flag, which belong in normal App Service Configuration instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: in the Part 2 Logic Apps problem scenario, polling a partner's REST API, the partner's API key is exactly the kind of secret that belongs in Key Vault - Managed Identity can't eliminate it, since the partner system has no concept of Azure AD identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 3: VNet Integration
&lt;/h2&gt;

&lt;p&gt;VNet Integration is a feature that lets Azure PaaS services, Function Apps, Logic Apps Standard, App Service, send their outbound traffic through a Virtual Network, rather than over the public internet, meaning they can reach resources that are locked down to only accept traffic from within a specific private network.&lt;/p&gt;

&lt;p&gt;Think of the difference between mailing a letter through the public postal system versus using a company's internal, private courier route between two buildings on the same secured campus. The destination might be completely unreachable to the public postal system at all - only the private internal route can get there.&lt;/p&gt;

&lt;p&gt;Without VNet Integration, a Function App reaches Azure SQL over the public internet, and Azure SQL's firewall must allow some public IP range to reach it, even if narrowly scoped. With VNet Integration, the Function App reaches Azure SQL through a private VNet route, paired with a Private Endpoint covered next, and Azure SQL can be configured to reject all public internet traffic entirely, reachable only via the private network path.&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;// Enabling VNet Integration - Azure Portal:&lt;/span&gt;
&lt;span class="c1"&gt;// Function App -&amp;gt; Networking -&amp;gt; VNet Integration -&amp;gt; Add VNet&lt;/span&gt;
&lt;span class="c1"&gt;// Select an existing VNet and a delegated subnet&lt;/span&gt;

&lt;span class="c1"&gt;// This affects OUTBOUND calls FROM the Function App -&lt;/span&gt;
&lt;span class="c1"&gt;// calls made to Azure SQL, Service Bus, Key Vault, or&lt;/span&gt;
&lt;span class="c1"&gt;// any other service can now route through the VNet&lt;/span&gt;
&lt;span class="c1"&gt;// instead of the public internet, IF those services&lt;/span&gt;
&lt;span class="c1"&gt;// are also configured to accept that private path&lt;/span&gt;
&lt;span class="c1"&gt;// (via Private Endpoints, covered next)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: VNet Integration requires a Premium or higher App Service or Function App plan - it's not available on the Consumption tier, so it's a genuine cost and architecture decision, not just a checkbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: a Function App processing sensitive order data, from the Part 2 problem scenarios, can use VNet Integration to reach Azure SQL entirely privately, meaning even if the Function App's HTTP endpoint were somehow compromised, an attacker still couldn't reach the database over the public internet at all - the network path itself doesn't exist publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 4: Private Endpoints vs Service Endpoints, the Interview Trap
&lt;/h2&gt;

&lt;p&gt;Both features let traffic from a VNet reach an Azure PaaS service more directly than the general public internet path, but they work in genuinely different ways, and mixing them up is a common, specific interview trap.&lt;/p&gt;

&lt;p&gt;A Service Endpoint means traffic still travels to the service's public IP address - it just takes an optimized route while doing so, and the service can be configured to only accept traffic originating from specific VNet subnets. The service still has a public IP and public DNS name, and a firewall rule on the service, such as Azure SQL's firewall, is what actually restricts access.&lt;/p&gt;

&lt;p&gt;A Private Endpoint means the Azure service gets an actual private IP address, genuinely inside your VNet's address space. DNS resolution for the service now resolves to that private IP, not a public one, when queried from within the VNet. The service can be configured to reject all public internet traffic entirely - not just restrict it by source, but genuinely have no public path at all.&lt;/p&gt;

&lt;p&gt;Think of a Service Endpoint as a VIP lane at a public building's entrance - you still arrive at the same public building, just through a faster, monitored door. A Private Endpoint is like the building itself being moved entirely inside a private, gated campus - there is no public entrance to speak of anymore.&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;// Private Endpoint setup - Azure Portal:&lt;/span&gt;
&lt;span class="c1"&gt;// Azure SQL Server -&amp;gt; Networking -&amp;gt; Private access&lt;/span&gt;
&lt;span class="c1"&gt;// -&amp;gt; Create a Private Endpoint -&amp;gt; select VNet + subnet&lt;/span&gt;

&lt;span class="c1"&gt;// After this, the SQL server's DNS name resolves to&lt;/span&gt;
&lt;span class="c1"&gt;// a PRIVATE IP address (e.g. 10.0.1.5) when queried&lt;/span&gt;
&lt;span class="c1"&gt;// from inside the VNet, instead of its public IP&lt;/span&gt;

&lt;span class="c1"&gt;// The SQL Server can then have "Deny public network&lt;/span&gt;
&lt;span class="c1"&gt;// access" enabled entirely - genuinely unreachable&lt;/span&gt;
&lt;span class="c1"&gt;// from the public internet, regardless of firewall&lt;/span&gt;
&lt;span class="c1"&gt;// rules, since there is no public path left at all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: Private Endpoints cost more, a per-hour charge per endpoint, and add real DNS configuration complexity, often requiring a Private DNS Zone to resolve correctly. Service Endpoints are simpler and free, but offer a weaker security boundary since the service technically remains publicly reachable in principle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: Service Bus, Azure SQL, and Key Vault, every exit point and dependency across the Part 1 and Part 2 pipelines, can each individually be configured with a Private Endpoint, meaning the entire pipeline from Part 2's complete picture diagram can be rebuilt to never touch the public internet at any internal hop, with only the true entry point, APIM, if external-facing, remaining publicly reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 5: Network Security Groups, NSGs
&lt;/h2&gt;

&lt;p&gt;A Network Security Group is a set of firewall rules applied at either a subnet level or an individual network interface level, controlling inbound and outbound traffic by source and destination IP, port, and protocol.&lt;/p&gt;

&lt;p&gt;Think of a building's security desk with a specific list of who's allowed in, through which door, carrying what. Traffic that doesn't match an explicit allow rule, and doesn't match a higher-priority deny rule, simply doesn't get through.&lt;/p&gt;

&lt;p&gt;A few core NSG concepts worth knowing precisely: rules have priority numbers from 100 to 4096, where lower numbers are evaluated first, and the first matching rule wins with evaluation stopping there. Rules specify direction, inbound or outbound, source, destination, port, protocol, and allow or deny. Default rules exist automatically and cannot be deleted, only overridden by a higher-priority custom rule - these include allowing VNet-to-VNet traffic, allowing Azure Load Balancer traffic, and denying all other inbound traffic by default. NSGs can be applied at two levels simultaneously, subnet level affecting everything in that subnet, and network interface level affecting one specific resource - both are evaluated if both exist, and traffic must pass both to actually get through.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Example NSG rule (conceptual, via Azure Portal or CLI)
Priority: 100
Name: Allow-HTTPS-Inbound
Direction: Inbound
Source: Internet
Destination: 10.0.1.0/24 (a specific subnet)
Port: 443
Protocol: TCP
Action: Allow

Priority: 200
Name: Deny-All-Other-Inbound
Direction: Inbound
Source: Any
Destination: Any
Port: Any
Action: Deny
// (this duplicates default behavior, but shown
// explicitly here for clarity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: NSGs are coarse-grained, IP, port, and protocol, compared to something like APIM's subscription key or OAuth validation, covered in the APIM posts on this blog. NSGs control whether traffic can reach a network destination at all, not who is making an authenticated request once it arrives. Real architectures layer both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: if Function Apps and Logic Apps in the Part 2 pipeline are deployed with VNet Integration into a specific subnet, an NSG on that subnet is what actually restricts which other subnets or IP ranges those services can communicate with - the network-level enforcement underneath the VNet Integration and Private Endpoint configuration already discussed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 6: Encryption in Transit vs At Rest
&lt;/h2&gt;

&lt;p&gt;These are two genuinely different protections, both necessary, neither substituting for the other. Encryption in transit protects data while it's actively moving between two points over a network. Encryption at rest protects data while it's sitting stored somewhere, on disk, in a database.&lt;/p&gt;

&lt;p&gt;Think of a sealed, tamper-evident courier bag that protects a letter while it travels, in transit, but once it arrives and gets filed away, the bag itself does nothing to protect the letter sitting in an unlocked filing cabinet, at rest. Both protections are needed, and they protect against different threats at different moments.&lt;/p&gt;

&lt;p&gt;Encryption in transit covers HTTPS and TLS for API calls, which APIM, Function Apps, and Logic Apps all enforce by default for their endpoints, and Service Bus and Azure SQL connections, encrypted via TLS by default. This is mostly already on by default across Azure services - the interview-relevant knowledge is knowing it's the default, and knowing how to verify it hasn't been accidentally disabled somewhere.&lt;/p&gt;

&lt;p&gt;Encryption at rest covers Azure Storage, Azure SQL, and Service Bus messages at rest, all encrypted by default using Microsoft-managed keys. Customer-managed keys, via Key Vault, are available for organizations needing to control their own encryption keys specifically - a real, sometimes compliance-driven choice, not the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: Microsoft-managed keys, the default, require zero setup and are genuinely secure for most scenarios. Customer-managed keys add real operational overhead, since you're now responsible for key rotation and availability, in exchange for direct control - typically only worth it when a specific compliance requirement mandates it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: every message sitting in a Service Bus queue, from Part 1, every order record written to Azure SQL by a Function App, from Part 2, is encrypted at rest by default without any explicit configuration - worth knowing this is already true, rather than assuming it needs to be manually enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 7: Azure RBAC, Role-Based Access Control
&lt;/h2&gt;

&lt;p&gt;Azure RBAC is the actual mechanism that decides what a given identity, a Managed Identity, a user, a group, is allowed to do once it has authenticated. Every grant-access step mentioned so far, a Function App reading a Key Vault secret, a Managed Identity publishing to Service Bus, has been RBAC quietly doing the actual authorization work underneath.&lt;/p&gt;

&lt;p&gt;Think of a hospital ID badge system, distinct from the building access badge used earlier for Managed Identity. The badge, the identity, gets you through the front door, but RBAC is the separate system deciding whether that specific badge lets you into the pharmacy, the operating room, or only the waiting area. Having a valid badge and being authorized for a specific room are two different checks.&lt;/p&gt;

&lt;p&gt;A few core RBAC concepts matter here. A Role Definition is a named set of permissions - "Key Vault Secrets User" can read secrets but not manage access policies, while "Key Vault Administrator" can do both. A Role Assignment binds a specific identity to a specific role at a specific scope - "this Managed Identity has the Storage Blob Data Reader role on this storage account." Scope determines where a role assignment applies, running from Management Group down through Subscription, Resource Group, to an individual Resource, and a role granted at a higher scope is inherited by everything underneath it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Granting RBAC access via Azure CLI - conceptually&lt;/span&gt;
&lt;span class="c"&gt;# what happens when you click "Add role assignment"&lt;/span&gt;
&lt;span class="c"&gt;# in the portal&lt;/span&gt;

az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee&lt;/span&gt; &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-app-managed-identity-object-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Service Bus Data Sender"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; /subscriptions/&lt;span class="o"&gt;{&lt;/span&gt;sub&lt;span class="o"&gt;}&lt;/span&gt;/resourceGroups/&lt;span class="o"&gt;{&lt;/span&gt;rg&lt;span class="o"&gt;}&lt;/span&gt;/providers/Microsoft.ServiceBus/namespaces/&lt;span class="o"&gt;{&lt;/span&gt;namespace&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# This Function App's Managed Identity can now SEND&lt;/span&gt;
&lt;span class="c"&gt;# messages to this specific Service Bus namespace -&lt;/span&gt;
&lt;span class="c"&gt;# nothing more, nothing less, and no connection&lt;/span&gt;
&lt;span class="c"&gt;# string was involved anywhere in granting this access&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The principle of least privilege, made concrete: a Function App that only ever needs to send messages to Service Bus should get the "Service Bus Data Sender" role specifically, not the broader "Service Bus Data Owner" role - even though both would technically make the code work, the narrower role means a compromised Function App identity still can't delete queues or read messages meant for other consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff&lt;/strong&gt;: RBAC role assignments add real setup overhead compared to a shared connection string that just works everywhere - the honest payoff is that a leaked or compromised identity has precisely bounded damage potential, rather than full access to whatever the shared secret happened to unlock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to everything already covered&lt;/strong&gt;: RBAC is the actual authorization layer underneath every Managed Identity example in this post - granting the Function App access to Key Vault, or the Managed Identity being able to publish to Service Bus, are both RBAC role assignments happening behind the portal UI. Managed Identity answers who is this, RBAC answers what are they allowed to do, the same Authentication vs Authorization distinction covered in depth in an earlier post on this blog, now applied specifically to Azure resource-to-resource access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topic 8: Token Validation, Revisited in This Context
&lt;/h2&gt;

&lt;p&gt;The JWT validation happening at the APIM gateway, covered in real depth in the APIM Part 1 post on this blog, deserves an explicit callback here, since Topic 7's RBAC and this token validation answer genuinely parallel questions at two different layers of the same pipeline.&lt;/p&gt;

&lt;p&gt;There are two layers here, answering two different questions, running at two different points in the same request. APIM's validate-jwt policy, the external entry point, validates a token presented by an external caller - is this token signed correctly, not expired, issued for the right audience - covered in depth in the APIM Part 1 post. Azure RBAC, internal and resource-to-resource, operates once inside the pipeline, deciding what an already-authenticated Azure resource, via Managed Identity, is allowed to do to another Azure resource. Both are genuinely "authorization" in the AuthN and AuthZ sense covered in an earlier dedicated post - one happens at the public edge, checking an external caller's claims; the other happens entirely within Azure's own control plane, checking a resource's own identity against RBAC role assignments.&lt;/p&gt;

&lt;p&gt;Where this distinction actually gets tested in an interview: being asked how an API is secured and only mentioning APIM's token validation misses half the real answer - a fully secured pipeline validates the external caller at the edge, APIM's JWT validation, and enforces least-privilege access for every internal hop afterward, RBAC on each Managed Identity. Mentioning only one of the two is the incomplete answer a panel interviewer is specifically listening for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this connects to Parts 1 and 2&lt;/strong&gt;: in the Part 2 end-to-end example, the external partner's token gets validated once, at APIM, at the very start of the pipeline - but every single hop afterward, Logic App to Function App, Function App to Key Vault, Function App to SQL, Function App to Service Bus, is a separate authorization decision, made by RBAC, not by re-checking that same original token. This is why both topics genuinely need to be understood together, not treated as the same concept covered twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the Whole Security Picture Together
&lt;/h2&gt;

&lt;p&gt;Revisiting the Part 2 end-to-end pipeline, now with security layered on top of every hop, tells the complete story. An external partner calls APIM as the entry point, with TLS enforced for encryption in transit, and a subscription key plus OAuth validated at the gateway. APIM forwards to a Logic App, ideally over a private path if the Logic App is VNet-integrated. The Logic App calls a Function App for complex transformation, authenticating using Managed Identity rather than a stored key. The Function App needs a partner API key for one step, retrieved from Key Vault, referenced via the Function App's own Managed Identity. The Function App writes the result to Azure SQL, reached via a Private Endpoint, with SQL configured to reject all public internet traffic entirely, and an NSG on the Function App's subnet permitting this specific outbound path. The Function App publishes to a Service Bus topic for the next stage, authenticated via Managed Identity and Azure RBAC, with no connection string secret anywhere. Every step logs to Application Insights regardless - security doesn't remove the need for observability from Part 2, it adds to it.&lt;/p&gt;

&lt;p&gt;This is the shape of answer, describing security as a layer across an already-understood pipeline rather than a separate topic, that demonstrates real architectural thinking in a panel interview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Managed Identity eliminates stored secrets for Azure-to-Azure authentication entirely - it doesn't help for non-Azure, third-party systems, which is exactly where Key Vault still matters.&lt;/p&gt;

&lt;p&gt;Key Vault and Managed Identity work together - the Managed Identity is the credential that authenticates to Key Vault, which then holds whatever secrets genuinely can't be eliminated.&lt;/p&gt;

&lt;p&gt;VNet Integration affects outbound traffic from a PaaS service; Private Endpoints affect how that service is reached - the two work together to keep an entire pipeline off the public internet.&lt;/p&gt;

&lt;p&gt;Service Endpoints and Private Endpoints are not the same thing - a Service Endpoint optimizes a still-public path, a Private Endpoint gives the service an actual private IP with no public path required at all.&lt;/p&gt;

&lt;p&gt;NSGs control network-level reachability, whether traffic can get there at all, while APIM's subscription keys and OAuth validation control authenticated access, who is allowed once traffic arrives - real architectures need both layers, not one instead of the other.&lt;/p&gt;

&lt;p&gt;Encryption in transit and at rest are both already on by default for most Azure services - the interview-relevant knowledge is knowing that default exists, not assuming it needs manual setup.&lt;/p&gt;

&lt;p&gt;RBAC is the authorization layer underneath every Managed Identity grant covered in this post - Managed Identity answers who is this, RBAC answers what are they allowed to do, and least-privilege role selection genuinely bounds the damage a compromised identity can do.&lt;/p&gt;

&lt;p&gt;Token validation at APIM, the external edge, and RBAC, internal and resource-to-resource, are both authorization but operate at genuinely different layers of the same pipeline - a complete answer to how something is secured needs both, not just one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Future parts of this series will cover additional Azure integration topics based on continued interview prep needs - debugging and monitoring the secured pipeline built across this and earlier parts, or deeper API Management security patterns specifically.&lt;/p&gt;

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

&lt;p&gt;Security and networking aren't a separate concern from the messaging and orchestration services covered in Parts 1 and 2 - they're a layer that sits directly on top of that same architecture. Managed Identity removes stored secrets between Azure services. Key Vault holds what genuinely can't be eliminated. VNet Integration and Private Endpoints keep traffic off the public internet entirely. NSGs enforce network-level reachability underneath all of it. Encryption in transit and at rest protect data whether it's moving or sitting still. Describing how these layer onto a specific, already-understood pipeline, rather than reciting them as abstract concepts, is what turns a security question into a demonstration of real architectural thinking in a panel interview.&lt;/p&gt;




&lt;p&gt;Originally published at TechStack Blog: &lt;a href="https://www.techstackblog.com/post.html?slug=azure-integration-interview-prep-part3" rel="noopener noreferrer"&gt;https://www.techstackblog.com/post.html?slug=azure-integration-interview-prep-part3&lt;/a&gt; &lt;br&gt;
More from TechStack Blog: Azure: &lt;a href="https://www.techstackblog.com/category.html?cat=azure" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=azure&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>interview</category>
      <category>security</category>
    </item>
    <item>
      <title>Azure Integration Services Interview Prep Part 2: Logic Apps, Function Apps, Durable Functions, and the Complete Orchestration Picture</title>
      <dc:creator>Manohari Jayachandran</dc:creator>
      <pubDate>Fri, 21 Aug 2026 22:35:49 +0000</pubDate>
      <link>https://dev.to/manoharij/azure-integration-services-interview-prep-part-2-logic-apps-function-apps-durable-functions-and-520e</link>
      <guid>https://dev.to/manoharij/azure-integration-services-interview-prep-part-2-logic-apps-function-apps-durable-functions-and-520e</guid>
      <description>&lt;p&gt;Part 1 covered Service Bus, Storage Queues, Event Hub, and Event Grid - the services that move data around a system. None of them actually do anything with that data once it arrives. This part covers the two services that do - Logic Apps and Function Apps - and just as importantly, connects them into the complete picture: what typically triggers this orchestration layer, and where the result actually goes afterward. A panel interview rarely asks about one service in isolation; it asks you to describe a whole pipeline, start to finish.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayev9gmvdykhbphkhbxf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayev9gmvdykhbphkhbxf.png" alt="Part 2" width="800" height="753"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3payvhog3ycs46k26xn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3payvhog3ycs46k26xn.png" alt="Complete pipeline" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Service 1: Azure Logic Apps
&lt;/h2&gt;

&lt;p&gt;Azure Logic Apps is a low-code, visual workflow orchestration service. Triggers and actions are connected in a designer, backed by hundreds of prebuilt connectors, Salesforce, ServiceNow, SQL, Service Bus, and many more, with retry policies and error handling configurable without writing code.&lt;/p&gt;

&lt;p&gt;Think of a flowchart that actually runs itself. Instead of drawing a diagram of "if this, then that" and handing it to a developer to implement, the flowchart is the running system - each box is a real, executing step, and the connectors are pre-wired plugs into other systems rather than something you build a client for yourself.&lt;/p&gt;

&lt;p&gt;Logic Apps fit connector-heavy orchestration where the actual logic is relatively straightforward, sequence, conditions, simple transformation, and being readable by a non-developer stakeholder, or having built-in retry and error handling with zero code, is a genuine advantage.&lt;/p&gt;

&lt;p&gt;Logic Apps' strengths include hundreds of prebuilt connectors for SaaS systems, databases, and Azure services with no custom auth or client code needed, a visual design reviewable by non-developers, built-in retry policies per action configurable without code, and fast build time for straightforward orchestration. The weaknesses are real too - complex branching logic becomes hard to read as a diagram, genuine unit testing is awkward compared to real code, and complex data transformation is technically possible in the expression language but painful compared to the equivalent C#.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logic Apps: A Real Example
&lt;/h3&gt;

&lt;p&gt;Consider a scenario where a new case is created in Salesforce. If the case priority is High, the workflow needs to create a matching incident in ServiceNow and notify the on-call engineer via Teams.&lt;/p&gt;

&lt;p&gt;Built as a Logic App, this becomes four steps: a trigger firing when a new record is created via the Salesforce connector, a Condition action checking whether Priority equals High, a Create Record action against the ServiceNow connector if true, and a Post Message action through the Microsoft Teams connector. All four steps are entirely visual, each with its own configurable retry policy, with no custom HTTP client code written for either Salesforce or ServiceNow - the connectors handle authentication and API specifics automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logic Apps: Problem Scenario and Solving Strategy
&lt;/h3&gt;

&lt;p&gt;The problem: a partner integration needs to pull new orders from an external partner's REST API every 15 minutes, check each order against three different validation rules, and if valid, insert it into Azure SQL - if invalid, send an email to the operations team with the specific reason. The integration needs to be reviewable by a non-technical operations lead, and built quickly, since this is a short-term partner relationship.&lt;/p&gt;

&lt;p&gt;The strategy, step by step: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, recognize the shape of this problem - scheduled polling, simple sequential validation, a database write, a conditional notification - this is connector-heavy orchestration with straightforward logic, not complex custom computation. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, use a Logic App with a Recurrence trigger set to 15 minutes. Third, add an HTTP action to call the partner's REST API and retrieve new orders. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fourth, use a For Each loop over the returned orders, with three Condition actions checking each validation rule in sequence. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fifth, branch on the combined result - valid orders go through a SQL connector action inserting a row into the Orders table, invalid orders trigger an Outlook or Office 365 connector action sending an email with the specific failed rule included in the message body. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sixth, configure a retry policy on the HTTP action specifically, exponential backoff with 3 retries, in case the partner's API is briefly unavailable. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, the non-technical reviewability requirement is satisfied inherently by the visual designer, requiring no additional work for that specific requirement.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Service 2: Azure Function Apps
&lt;/h2&gt;

&lt;p&gt;Azure Function Apps provide serverless compute that runs a specific piece of C#, or other language, code in response to a trigger, billed per execution rather than for a server sitting idle. This was covered in more depth in an earlier post on this blog.&lt;/p&gt;

&lt;p&gt;Think of a specialist called in for one specific job, paid only for the time actually worked, rather than a full-time employee sitting at a desk waiting for something to do. The specialist shows up when triggered, does the specific task, and leaves, with no idle overhead.&lt;/p&gt;

&lt;p&gt;Function Apps fit genuinely complex logic, custom validation with many conditions, algorithms too sophisticated for a visual designer's expression language, and anywhere real unit testing of the logic matters.&lt;/p&gt;

&lt;p&gt;Function Apps' strengths include real C# with full language power and genuine testability, six trigger types covering HTTP, Timer, Service Bus, Blob, Queue, and Event Grid, bindings that eliminate boilerplate connection code, and Durable Functions handling workflows beyond the 10-minute execution limit. The weaknesses include no built-in visual retry configuration, requiring retry logic to be written explicitly in code, commonly with Polly, less transparency to a non-developer reviewer, and more upfront setup than dragging in a connector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function Apps: A Real Example
&lt;/h3&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="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ValidateAndTransformOrder"&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;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"incoming-orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="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;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RawOrder&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="c1"&gt;// Complex validation logic - genuinely easier to&lt;/span&gt;
    &lt;span class="c1"&gt;// read, test, and maintain as real code than as a&lt;/span&gt;
    &lt;span class="c1"&gt;// Logic App expression chain&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&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="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;100000&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;ValidationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Amount out of range"&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="nf"&gt;IsValidSku&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;ProductCode&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;ValidationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown product code"&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;transformed&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;Id&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;ExternalId&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;NormalizedSku&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;NormalizeSkuFormat&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;ProductCode&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ProcessedAt&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SaveToDbAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;log&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;"Order {Id} processed successfully"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transformed&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Function Apps: Problem Scenario and Solving Strategy
&lt;/h3&gt;

&lt;p&gt;The problem: incoming order data from Service Bus needs to go through a genuinely complex transformation - reconciling product codes against three different SKU formats used historically, applying a multi-step pricing adjustment algorithm with several conditional tiers, and validating against business rules that involve checking multiple related fields together. The team needs to unit test this logic directly, since a bug here has real financial impact.&lt;/p&gt;

&lt;p&gt;The strategy, step by step: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, recognize this is not connector-heavy orchestration - it's genuinely complex, multi-step business logic that needs to be read, tested, and maintained as real code, which immediately points to Function Apps over Logic Apps. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, use a Function App with a Service Bus Trigger, consuming messages from the incoming-orders queue directly, since Part 1 already covered why Service Bus fits this scenario upstream, given ordering and reliability matter for order data. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third, structure the transformation logic as a separate, independently testable class, such as OrderTransformer, rather than inline in the function itself, since this is what actually enables genuine unit testing. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fourth, write xUnit tests directly against OrderTransformer, covering each SKU format, each pricing tier boundary, and each validation rule combination - something not realistically possible against a Logic App's visual workflow. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fifth, use Polly for explicit retry logic around any external call within the transformation, such as an external SKU lookup service, since Function Apps don't get this for free the way Logic Apps' connectors do. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, on successful transformation, complete the Service Bus message; on failure, let it retry per Service Bus's own retry policy, eventually dead-lettering after max attempts for manual investigation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Durable Functions: State Management Beyond a Single Execution
&lt;/h2&gt;

&lt;p&gt;A Durable Function is a Function App extension that lets you write long-running, stateful workflows in code - workflows that can span minutes, hours, or even months, well beyond a regular function's execution time limit. Durable Functions automatically checkpoints progress to Azure Storage, meaning the workflow's state survives restarts, scaling events, or the host process recycling entirely.&lt;/p&gt;

&lt;p&gt;Think of a video game's save file. A regular Function App is like a game with no save feature - if you close it, all progress is lost, you start over from the beginning. Durable Functions automatically saves your progress at each checkpoint, so if the process restarts, equivalent to your console crashing, the workflow resumes exactly where it left off, rather than starting over.&lt;/p&gt;

&lt;p&gt;There are three function types in Durable Functions. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Orchestrator Function coordinates the overall workflow, calling Activity Functions in sequence or in parallel, handling retries and timeouts, with its own execution state automatically checkpointed. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Activity Function does the actual work - calling an external API, querying a database, running a calculation - and each one is a normal, stateless function under the hood. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Client Function starts a new orchestration instance, typically an HTTP trigger that kicks off the workflow and returns an instance ID for checking status later.&lt;br&gt;
&lt;/p&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;// Client function - starts the workflow&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"StartOrderWorkflow"&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;HttpResponseData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StartWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;HttpRequestData&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DurableClient&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;DurableTaskClient&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;{&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;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;instanceId&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScheduleNewOrchestrationInstanceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"ProcessOrderOrchestrator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateCheckStatusResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instanceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Orchestrator function - coordinates the workflow&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ProcessOrderOrchestrator"&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="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RunOrchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrchestrationTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;TaskOrchestrationContext&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;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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetInput&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Sequential activity calls - each one checkpointed&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;validated&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;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"ValidateOrder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;validated&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;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order validation failed"&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;paymentResult&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;CallActivityAsync&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="s"&gt;"ChargePayment"&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="c1"&gt;// A durable TIMER - the orchestrator can genuinely&lt;/span&gt;
    &lt;span class="c1"&gt;// "sleep" for hours or days without consuming any&lt;/span&gt;
    &lt;span class="c1"&gt;// compute during the wait, unlike Task.Delay&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;CreateTimer&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;CurrentUtcDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&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;confirmed&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;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"SendConfirmationAndAwaitAck"&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="c1"&gt;// Fan-out/fan-in - run several activities in&lt;/span&gt;
    &lt;span class="c1"&gt;// PARALLEL, then wait for all to complete&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&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="n"&gt;Task&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UpdateInventory"&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NotifyWarehouse"&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UpdateAnalytics"&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="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Activity function - the actual work, stateless&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ValidateOrder"&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="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ValidateOrder&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ActivityTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Order&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;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="n"&gt;Amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&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;IsNullOrEmpty&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;ProductCode&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 checkpointing works like this: every time an orchestrator awaits an activity or a timer, its current execution state is saved to Azure Storage automatically. If the process crashes or restarts mid-workflow, the orchestrator function actually replays from the beginning when it resumes - but any activity call that already completed simply returns its already-saved result instantly instead of re-executing, so the orchestrator effectively fast-forwards back to exactly where it left off. This replay behavior is why activity functions, the actual work, must be deterministic-safe to call multiple times, or genuinely idempotent, and why non-deterministic operations like DateTime.Now or Guid.NewGuid() should be called through provided context methods rather than directly, inside an orchestrator specifically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable Functions: Problem Scenario and Solving Strategy
&lt;/h3&gt;

&lt;p&gt;The problem: an order approval workflow needs to send a request to a manager for approval, then wait for up to 5 business days for a response. If approved, proceed with fulfillment. If no response within 5 days, escalate to a senior manager automatically. A regular Function App's 10-minute execution limit makes this directly impossible to build as a single running function, and the system needs to survive deployments, restarts, and scaling events without losing track of orders waiting on approval.&lt;/p&gt;

&lt;p&gt;The strategy, step by step: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First, recognize this is fundamentally a long-running, stateful workflow problem - the multi-day wait alone rules out a regular Function App entirely, and rules out Logic Apps too if genuinely complex branching logic around the escalation needs real code. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Second, use Durable Functions specifically for its durable timer capability, since the orchestrator can wait for days without consuming compute the whole time, and survives restarts because its state is checkpointed to storage. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Third, build a Client Function, an HTTP trigger, that starts the orchestration when an order needs approval, returning an instance ID. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fourth, build the Orchestrator Function to call an Activity Function that sends the approval request via email or Teams notification to the manager, then use context.WaitForExternalEvent for an "ApprovalReceived" event combined with context.CreateTimer for the 5-day deadline, racing both with Task.WhenAny. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fifth, if the ApprovalReceived external event arrives first, proceed to a ProcessApproval Activity Function. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sixth, if the timer fires first, 5 days passed with no response, call an EscalateToSeniorManager Activity Function instead. Seventh, the manager's actual approval action, such as clicking a link in an email, calls a separate HTTP-triggered function that raises the external event back into the waiting orchestration instance using its instance ID - this is what wakes up the specific waiting workflow. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, because state is checkpointed automatically, this entire multi-day wait survives app restarts, deployments, or scaling events with zero custom state-persistence code written by hand.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Logic Apps, Function Apps, or Durable Functions
&lt;/h2&gt;

&lt;p&gt;Choose Logic Apps when the work is connector-heavy orchestration, the branching logic is straightforward, and visual reviewability matters. Choose regular Function Apps when the logic is complex, genuine unit testing is needed, and execution completes well within the 10-minute limit. Choose Durable Functions when the workflow needs to span minutes to months, requires genuine state that survives restarts, needs fan-out and fan-in parallelism with a final aggregation step, or needs to wait for an external event, like a human approval, without holding compute the whole time. Choose Logic Apps and Function Apps together when a Logic App can orchestrate the overall connector-heavy flow, calling out to a Function App as one action for the specific step complex enough to warrant real code - a common, often correct real-world pattern, covered in more depth in an earlier post on this blog comparing the two services directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete Picture: Entry Points
&lt;/h2&gt;

&lt;p&gt;A panel interview rarely stops at "which orchestration service" - it typically wants the whole pipeline. Here's what actually triggers this layer in real architectures.&lt;/p&gt;

&lt;p&gt;Azure API Management as an entry point means an external caller hits an API exposed through APIM, covered in depth in earlier posts on this blog. APIM validates the caller through a subscription key or OAuth, applies rate limiting, then forwards to a Logic App or Function App as the actual backend. This fits external, internet-facing integrations needing authentication and governance at the edge.&lt;/p&gt;

&lt;p&gt;Service Bus as an entry point means a message arrives on a queue or topic, and a Function App with a Service Bus Trigger, or a Logic App's Service Bus connector trigger, picks it up automatically. This fits internal, decoupled, asynchronous processing, where the sender doesn't need to wait for the orchestration to complete.&lt;/p&gt;

&lt;p&gt;Event Grid as an entry point means a discrete event fires - a blob uploaded, an Azure resource changed, a custom application event - and Event Grid pushes it directly to a subscribed Function App or Logic App. This fits reacting to something happening, not processing a queued backlog of work.&lt;/p&gt;

&lt;p&gt;Timer or Recurrence as an entry point means a scheduled trigger fires on a defined interval, independent of any external event. This fits polling an external system, running a nightly batch job, or periodic cleanup and reconciliation.&lt;/p&gt;

&lt;p&gt;Blob Storage as an entry point means a file lands in a specific container, triggering processing directly. This fits file-based integrations, where a partner drops a CSV or an export needs processing on arrival.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete Picture: Exit Points
&lt;/h2&gt;

&lt;p&gt;Azure SQL or Cosmos DB as an exit point means the orchestration's result is persisted, and the actual business record now exists in a queryable store. The choice between SQL and Cosmos DB follows the reasoning covered in an earlier post on this blog - relational, consistent shape versus flexible, fast-changing document data.&lt;/p&gt;

&lt;p&gt;Service Bus as an exit point means the orchestration hands off to the next stage rather than the pipeline ending here, publishing a message for a downstream process to pick up - common in multi-stage pipelines where each stage does one focused job.&lt;/p&gt;

&lt;p&gt;An external API as an exit point means the orchestration calls out to a partner system - ServiceNow, Salesforce, a third-party vendor API - completing the actual business integration the whole pipeline exists to support.&lt;/p&gt;

&lt;p&gt;Application Insights as an exit point applies always, regardless of the above. Every step logs its outcome, success, failure, duration, covered in depth in an earlier post on this blog about KQL and observability. This isn't optional in a well-built pipeline; it's how anyone, including you at 2am, actually knows what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the Whole Picture Together
&lt;/h2&gt;

&lt;p&gt;Here's a realistic end-to-end example, tying Part 1 and Part 2 together completely. An external partner calls an API exposed through APIM, with a subscription key and OAuth validated at the gateway. APIM forwards the validated request to a Logic App. The Logic App orchestrates the flow: it calls a validation step, then hands off the complex transformation to a Function App as one action within the same workflow. The Function App applies business logic, then publishes the transformed result to a Service Bus topic from Part 1, decoupling this pipeline from whatever consumes the result next. A separate Function App, triggered by a Service Bus subscription on that topic, picks up the message, writes the final record to Azure SQL, and calls an external API to notify the partner's system the order was received. Every step along this entire chain logs to Application Insights, so the complete journey of one specific order can be traced end to end using operation_Id, exactly as covered in the KQL monitoring post on this blog.&lt;/p&gt;

&lt;p&gt;This is the shape of answer that actually lands well in a panel interview - not naming one service in isolation, but describing how several connect into a coherent, traceable pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Lessons
&lt;/h2&gt;

&lt;p&gt;Logic Apps and Function Apps solve overlapping but genuinely different problems - connector-heavy visual orchestration versus complex, testable custom code.&lt;/p&gt;

&lt;p&gt;Durable Functions solve a third, distinct problem - genuine state that survives restarts, workflows spanning minutes to months, and waiting for external events without holding compute the whole time - none of which regular Function Apps or Logic Apps handle natively.&lt;/p&gt;

&lt;p&gt;Many real production architectures use both together - a Logic App orchestrating the overall flow, calling into a Function App for the specific step complex enough to warrant real code.&lt;/p&gt;

&lt;p&gt;A complete integration pipeline has an entry point, APIM, Service Bus, Event Grid, Timer, or Blob Storage, an orchestration layer, Logic App and/or Function App, and an exit point, SQL/Cosmos DB, Service Bus, or an external API, with Application Insights watching every step regardless.&lt;/p&gt;

&lt;p&gt;Describing the full pipeline, not just one isolated service, is what a panel interview is actually listening for when it asks an open-ended architecture question.&lt;/p&gt;

&lt;p&gt;The entry point shapes what's realistic downstream - a Service Bus entry implies async, decoupled processing; an APIM entry implies a synchronous external caller waiting for a response.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Future parts of this series will cover additional Azure integration topics - deeper API Management patterns, Azure AD and Entra ID authentication flows applied specifically to service-to-service integration scenarios, and more complete end-to-end architecture walkthroughs.&lt;/p&gt;

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

&lt;p&gt;Logic Apps and Function Apps are the orchestration layer sitting between Part 1's messaging services and wherever the result of an integration actually needs to go. Logic Apps excel at connector-heavy, visually reviewable workflows; Function Apps excel at complex, genuinely testable business logic, and real architectures frequently combine both. The complete picture matters as much as any individual service choice: knowing what typically triggers this layer, and where the result goes afterward, is what turns a list of memorized services into a coherent architecture a panel interviewer can follow from entry to exit.&lt;/p&gt;




&lt;p&gt;Originally published at TechStack Blog: &lt;a href="https://www.techstackblog.com/post.html?slug=azure-integration-interview-prep-part2" rel="noopener noreferrer"&gt;https://www.techstackblog.com/post.html?slug=azure-integration-interview-prep-part2&lt;/a&gt; &lt;br&gt;
More from TechStack Blog: Azure: &lt;a href="https://www.techstackblog.com/category.html?cat=azure" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=azure&lt;/a&gt;&lt;br&gt;
CS Fundamentals: &lt;a href="https://www.techstackblog.com/category.html?cat=cs-fundamentals" rel="noopener noreferrer"&gt;https://www.techstackblog.com/category.html?cat=cs-fundamentals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>interview</category>
      <category>integration</category>
    </item>
  </channel>
</rss>
