<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Thoughts and Musings — Architect4Hire</title>
        <link>https://www.architect4hire.com/blog</link>
        <description>Field notes on .NET and Azure architecture, legacy modernization, and AI-assisted development that survives contact with production.</description>
        <language>en-us</language>
        <lastBuildDate>Wed, 08 Jul 2026 12:57:28 GMT</lastBuildDate>
        <atom:link href="https://www.architect4hire.com/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title>One Team, Three Projects, One Board: Building PlannerPro With Layered SCRUB</title>
            <link>https://www.architect4hire.com/blog/one-team-three-projects-one-board-building-plannerpro-with-layered-scrub</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/one-team-three-projects-one-board-building-plannerpro-with-layered-scrub</guid>
            <pubDate>Wed, 08 Jul 2026 12:57:28 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>SCRUB Framework</category>
            <description>Every project management tool assumes a project has a team. This client had it backward — one team carrying three projects, and a senior developer with nine items in progress that no tool could see. So I built the board that could, using layered SCRUB prompts.</description>
            <content:encoded><![CDATA[<p><em>Or: what happens when every tool on the market assumes a fact about your team that isn&#39;t true</em></p>
<p>TL;DR — the code and the full prompt library live at <a href="https://github.com/architect4hire/plannerpro" target="_blank" rel="noopener noreferrer">https://github.com/architect4hire/plannerpro</a>. The prompts document at <code>docs/PLANNERPRO-SCRUB-PROMPTS.md</code> is the interesting part. This post explains why.</p>
<hr>
<p>I want to tell you about a standup.</p>
<p>Not a dramatic one. Nobody quit. Nothing shipped broken. The kind of standup that&#39;s much worse than that — the kind that happens every single morning, takes forty minutes, and produces less clarity than it started with.</p>
<p>The client had one delivery team. Six people. Good people. And at the moment I was engaged, that one team was carrying three completely independent projects: a customer portal rebuild, an internal reporting migration, and a compliance-driven integration with a hard regulatory date. Three backlogs. Three stakeholders. Three sets of priorities that had never once been in the same room together.</p>
<p>Their tooling handled this the way every project management tool handles it: three boards. Because every tool on the market is built on the same silent assumption — that a project has a team. One project, one board, one team, tidy.</p>
<p>This team had the assumption backward. The <em>team</em> had projects. Three of them. And the tooling had no answer for that at all.</p>
<p>So, the standup was tab-switching. Board one, board two, board three, back to board one because someone remembered something. The team lead kept a spreadsheet on the side — the real board, the one that actually reflected reality — updated by hand, wrong by Wednesday. And the thing that finally made someone call me: their most senior developer had nine items in progress. Nine. Three per board, and each board looked completely reasonable in isolation. No single view existed anywhere in the company that could show that number. The person carrying the most risk in the org was invisible to every tool they&#39;d paid for, precisely because the risk was spread across three views that never met.</p>
<p>That&#39;s not a process problem. That&#39;s a data model problem. The unit of capacity is the <em>person</em>, and every board they owned modeled capacity as a property of the <em>project</em>.</p>
<p>I couldn&#39;t find a tool that modeled it correctly without an enterprise license and a consulting engagement to configure it. So I built one. It&#39;s called PlannerPro; it&#39;s small, and it does exactly one thing the big tools don&#39;t: one board, columns for workflow state, swimlanes for projects, and work-in-progress limits that belong to the human being — enforced across every project simultaneously.</p>
<p>This post is about what I built, but mostly it&#39;s about <em>how</em> I built it — because PlannerPro went from an empty repository to a working, tested product using a committed library of small, layered prompts, and the discipline behind those prompts is the part you can steal.</p>
<hr>
<h2 id="scrub-for-those-just-joining-us">SCRUB, For Those Just Joining Us</h2>
<p>If you read <a href="/blog/the-prompt-is-the-plan-how-i-built-a-1-200-line-angular-component-with-a-framework-youve-never-heard-of">the BrandForge post</a>, you know the framework. Short version for everyone else.</p>
<p>AI coding tools have a failure mode I call the Plausibility Trap: the output isn&#39;t wrong in ways that crash. It&#39;s wrong in ways that compile, pass tests, look idiomatic, and quietly encode a domain decision you never made. The model knows how to write code. It does not know your domain. When your prompt leaves a gap, the model fills it with the most statistically common answer from training data — and the most common answer is, occasionally and silently, the wrong one for <em>you</em>.</p>
<p>SCRUB is the checklist that closes those gaps before you hit submit. Five elements, every prompt:</p>
<ul>
<li><strong>S — Scope.</strong> Exactly what you&#39;re asking for. Named methods, named files, nothing else.</li>
<li><strong>C — Constraints.</strong> The technical rules: framework versions, architecture, naming, DI style. <em>Your</em> codebase, not the training-data average.</li>
<li><strong>R — Restrictions.</strong> What you explicitly do not want, tiered CRITICAL / IMPORTANT / PREFERRED, each with a reason. The element everyone skips. The element that matters most.</li>
<li><strong>U — Usage.</strong> Who runs this, under what conditions, with what compliance and audit expectations?</li>
<li><strong>B — Behavior.</strong> What must not change. The star of every edit to existing code.</li>
</ul>
<p>I&#39;ve written a book about the full system — <em>Pratical AI Assisted Development</em>, out soon — which goes deep on tiered restrictions, Edit Mode patterns, prompt chaining, and applying the framework to codebases you inherited rather than started. But you don&#39;t need the book to follow this post. You need one idea from it:</p>
<p><strong>A prompt is a specification, and specifications deserve engineering.</strong></p>
<p>PlannerPro is what that looks like on a greenfield project, end to end.</p>
<hr>
<h2 id="the-architecture-and-the-choices-behind-it">The Architecture and the Choices Behind It</h2>
<p>Let me get the stack out of the way, then talk about the decisions that actually mattered.</p>
<p>.NET 10 Web API on the backend. EF Core 10 over SQL Server. Angular 20 on the front, standalone components, signals for all component state, Angular CDK for the drag-and-drop. If you&#39;ve read anything else I&#39;ve written, none of that surprises you.</p>
<p>Here&#39;s the first real decision: <strong>PlannerPro is one API project.</strong> Not microservices. Not Aspire orchestration. One Web API, one database, one Angular app.</p>
<p>I want to dwell on that for a second, because I&#39;ve spent a lot of this blog building distributed systems, and I can feel some of you raising an eyebrow. The honest architect&#39;s answer is that this system has one team of users, one write path that matters, and no independent scaling or deployment pressures anywhere. Microservices are a solution to organizational and operational problems that PlannerPro lacks. Reaching for them anyway would have been resume-driven development, and the whole point of this engagement was to solve a problem in days, not to build a platform. Right-sizing is an architectural decision too — arguably the one clients pay me for most often.</p>
<p>Inside that one project, the layering is strict, and it&#39;s the same layering I use everywhere: <strong>Controller → Facade → BusinessManager → DataManager → DbContext.</strong> Hand-rolled mapping extensions, no AutoMapper. Audit fields on every entity. Soft delete everywhere, enforced with a global query filter. Boring, deliberate, and — this matters for what comes later — <em>predictable enough that an AI can be pointed at it and told to conform</em>.</p>
<p>The domain model consists of three entities. Project. WorkItem. TeamMember. That&#39;s it — no sprints, no epics, no comments, no attachments. Version one is the board and the rule, and everything I declined to build was declined on purpose.</p>
<p>And the rule is the whole product, so let me state it precisely:</p>
<p><strong>A team member&#39;s WIP limit applies to their in-progress items across all projects.</strong> The query that backs it — <code>CountInProgressForMemberAsync</code> — deliberately has no project filter. There is a CRITICAL restriction in the prompt library that says exactly that: <em>do not filter this count by project</em>. Because an AI generating a data manager for a project-management tool will absolutely scope that query to the project. Every example in its training data does. Project-scoped WIP is the idiom. Project-scoped WIP is also, for this client, the <em>entire disease</em>. Their senior dev with nine in-progress items looked fine on every board precisely because every count was project-scoped. One <code>.Where()</code> clause, present or absent, is the difference between a tool that solves the problem and a tool that re-implements it with nicer CSS.</p>
<p>That&#39;s the Plausibility Trap in one sentence, by the way. The wrong version compiles, passes tests, and demos beautifully.</p>
<p>Two more decisions worth naming:</p>
<p><strong>The board is one payload.</strong> A single <code>GET /api/board</code> returns everything the screen needs, grouped and ready to render — the Angular component does no client-side regrouping. When five people are staring at a shared screen in standup, you want one fetch, one render, one source of truth.</p>
<p><strong>The server is the transition authority.</strong> Status moves one step forward or one step back — Backlog → Ready → In Progress → Review → Done, no skipping — and the <em>server</em> enforces it. The UI does a drag with an optimistic update and rolls back on rejection. I explicitly restricted the AI from greying out &quot;illegal&quot; columns during a drag, which is the clever UX idea it will offer you unprompted. A greyed-out column that&#39;s wrong is a lie with good intentions; an honest 422 toast after the fact is merely an inconvenience. When client-side cleverness and server truth disagree, cleverness loses.</p>
<p>One drag-and-drop restriction earns special mention because it&#39;s my favorite in the whole library: <strong>drop lists are connected only within a swimlane.</strong> A card can move between columns, never between projects. Why CRITICAL and not IMPORTANT? Because cross-lane drag isn&#39;t a UX bug — it&#39;s a <em>data corruption</em> bug wearing a UX feature&#39;s clothes. Dragging a card one lane down would silently reassign a work item to a different project, and the CDK will happily wire all the lists together if you don&#39;t tell it otherwise, because connecting everything is what the examples do. One restriction. One line. An entire category of &quot;why is this ticket in the wrong project&quot; support conversations, never born.</p>
<hr>
<h2 id="the-prompts-small-on-purpose">The Prompts: Small on Purpose</h2>
<p>Now the part I actually want to talk about.</p>
<p>There&#39;s a file in the repository — <a href="https://github.com/architect4hire/plannerpro" target="_blank" rel="noopener noreferrer"><code>docs/PLANNERPRO-SCRUB-PROMPTS.md</code></a> — that existed before any of the code did. Nineteen prompts. It&#39;s the plan I worked from, committed alongside the code it produced, and it follows the same chain the BrandForge component did: <strong>Context → Plan → Execute → Verify.</strong></p>
<p>But the style of these prompts is different from what most people write, and the difference is the point. These are <em>micro</em> prompts. Every one of them fits in a paragraph. The Scope blocks name exact methods with exact signatures. The Restrictions come tiered, and every CRITICAL restriction carries its reason — not as documentation garnish, but because the reason is the thing that tells the model <em>why</em> the plausible default is wrong here.</p>
<p>Compare the mega-prompt approach — &quot;build me a kanban tool with three projects and WIP limits, here are 40 requirements&quot; — with what&#39;s in that file. The mega-prompt produces two thousand lines in one shot. Somewhere in those two thousand lines, the WIP query has a project filter, the drop lists are cross-connected, and the rollback path doesn&#39;t exist. You will find those problems the way my client found their nine-item developer: later, in production, by accident.</p>
<p>The micro prompts produce the same two thousand lines. But no single generation is ever more than a couple hundred of them, and every one of those chunks got a review that took two minutes instead of an afternoon.</p>
<p>Each prompt also names its <strong>mode</strong>, because the tools are not interchangeable. Claude Code Chat for the layers where thinking matters — contracts, business rules, orchestration methods. Copilot Agent Mode for the layers that are mechanical and multi-file — data access, template slices, CSS — where the autonomous build-and-verify loop earns its keep. Claude Code Edit Mode for surgery after the fact, with Behavior as the dominant element. Reasoning in Chat, generation in Agent, precision in Edit. Matching the mode to the cognitive work is half the discipline.</p>
<hr>
<h2 id="why-layers-are-the-whole-ballgame">Why Layers Are the Whole Ballgame</h2>
<p>If you take one thing from this post, take this section.</p>
<p>The single most important structural decision in the prompt library is not any individual restriction. It&#39;s that <strong>the work is layered</strong>, and each layer constrains the next.</p>
<p>The backend is four layers. Layer 1 is contracts — interfaces, enums, entity skeletons, view model shapes. Signatures only, no bodies, maybe eighty lines. Layer 2 is data and mapping: the DbContext, the data managers, the mapping extensions. Layer 3 is the business rules — the transition table and the WIP check, the two things that <em>are</em> the product. Layer 4 is the HTTP surface and the wiring.</p>
<p>The frontend gets the same treatment, six layers deep: a signal skeleton with every method stubbed, then the template built in vertical slices — header and WIP strip, then the swimlane grid, then cards and drag wiring — then the method bodies, then the styles. The methods layer comes second to last deliberately: by the time the AI writes <code>onDrop()</code>, the entire template it orchestrates already exists in front of it. It is not writing orchestration in a vacuum and hoping the UI that arrives later happens to match.</p>
<p>Why does layering matter this much? Four reasons, in ascending order of importance.</p>
<p><strong>First, review becomes possible.</strong> Eighty lines of contracts is a code review. Two thousand lines of everything is a skim, and a skim is where the Plausibility Trap lives. Every ordering bug I&#39;ve ever shipped from AI-generated code went out through a review I did too fast because the diff was too big.</p>
<p><strong>Second, errors stay local.</strong> When Layer 2 has a mapping bug — and mapping bugs are the most common failure at that layer — I fix it with a Layer 2 prompt. Layers 3 and 4 never hear about it, because they reference Layer 2 through its contracts, not its internals. In a one-shot generation, a bug in one region sprays corrections across the entire output, and every correction risks disturbing something that was right.</p>
<p><strong>Third, naming compounds.</strong> Layer 1 is where I spend disproportionate review time, because the names chosen there propagate through every subsequent layer. A bad facade method name in Layer 1 costs five minutes to fix in Layer 1 and a five-file threading exercise to fix after Layer 4. Cheap-to-fix and expensive-to-fix are the same mistake at different timestamps; layering lets you always pay the cheap price.</p>
<p><strong>Fourth — and this is the one that changes how you work — each layer constrains the next.</strong> By Layer 3, the AI isn&#39;t inventing; it&#39;s <em>conforming</em>. The contracts exist. The data managers exist. The custom-instruction files pre-load the conventions. The generation space has been narrowed, layer by layer, until the plausible output and the correct output are nearly the same thing. That&#39;s the real trick. You&#39;re not hoping the model guesses right. You&#39;re removing its opportunities to guess.</p>
<p>The ordering echoes onion architecture, but the mapping isn&#39;t the point — the <em>size</em> is the point, and the direction of dependency is the point. Small enough to read in two minutes. Ordered so the shape exists before the behavior, and the behavior exists before the wiring.</p>
<p>And it doesn&#39;t feel slow. I expected it to, the first time. Nineteen prompts for a two-project solution? It felt fast, because I was never working on more than one small piece at a time, and I never once entered the mode every developer knows too well — spelunking through a giant generated diff looking for the line that&#39;s lying to you.</p>
<p>Fix the plan, not the code. If the plan is wrong, you fix a paragraph.</p>
<hr>
<h2 id="verify-the-step-that-catches-you-moving-too-fast">Verify: The Step That Catches You Moving Too Fast</h2>
<p>The chain ends with Verify prompts — one for the backend, one for the board component. Each is a list of specific PASS/FAIL checks the AI runs against the finished code, with file-and-line evidence required for any FAIL.</p>
<p>Not &quot;does it work.&quot; Specific. <em>Does the WIP count query have a project filter? Is any drop list connected across lanes? Does the rollback restore captured state, or does it cheat and re-fetch the whole board? Does any orchestration method contain a <code>.subscribe()</code>?</em> Every check on those lists maps back to a CRITICAL or IMPORTANT restriction from an earlier layer, because the Verify step exists for exactly one scenario: the day you review a diff too fast. On BrandForge, Verify caught a real bug my eyes had slid over. It stays in the chain forever.</p>
<p>The Verify lists are also the maintenance contract. When PlannerPro grows — per-column WIP limits are the obvious next ask, and a &quot;blocked&quot; flag is right behind it — the prompts document gets updated <em>first</em>: new restrictions, new Verify checks, then run the prompts. The document is versioned, committed, and reviewed like the code, because it is the reason the code looks the way it does. Six months from now, when somebody wonders why <code>CountInProgressForMemberAsync</code> pointedly ignores the project, the answer isn&#39;t in a comment or in my head. It&#39;s in a file with a commit history.</p>
<hr>
<h2 id="what-the-client-got">What the Client Got</h2>
<p>A board their standup actually runs on. Swimlanes for the three projects, five columns, drag-and-drop with honest server-side rules, and a WIP strip across the top showing every team member&#39;s cross-project count — the number that used to live in a hand-maintained spreadsheet, wrong by Wednesday.</p>
<p>The nine-item developer, by the way, is now capped at three, and the standup conversation changed shape the first week: it stopped being three sequential status reports and started being an argument about which project gets the constrained person next. That&#39;s the right argument. The tool didn&#39;t resolve their priority conflict — no tool does — but it made the conflict <em>visible on one screen</em>, which is the precondition for resolving it like adults.</p>
<p>And I got a repository where every line of code traces back to a small, reviewed, committed prompt — which means the next feature starts from a plan, not from archaeology.</p>
<hr>
<h2 id="the-practical-version">The Practical Version</h2>
<p>If you want to run this pattern on your next greenfield build:</p>
<p><strong>Write the domain brief first, and make the AI recite it back.</strong> One paragraph. The Context step costs five minutes and catches misunderstandings while they cost one sentence to fix.</p>
<p><strong>Argue with the Plan.</strong> In prose, before any code exists. The plan step is the cheapest insurance in the entire chain, and it is the step everyone skips.</p>
<p><strong>Layer the Execute.</strong> Contracts, then data, then rules, then wiring. Four to seven layers, each describable in three sentences. If a layer needs a fourth sentence, split it.</p>
<p><strong>Write Restrictions before Scope, and tier them.</strong> Ask what could compile and still be wrong. CRITICAL means silent wrong data. IMPORTANT means debt. PREFERRED means style. Attach reasons to the CRITICAL ones — the reason is what teaches the model why the idiom is wrong here.</p>
<p><strong>Find your no-project-filter.</strong> Every domain has one: the single line where the training-data idiom and your business reality point in opposite directions. Yours might be a tip default, a rounding rule, a scoped query. Find it before the AI does, and pin it with a CRITICAL restriction.</p>
<p><strong>Match the mode to the work.</strong> Chat for contracts, rules, and orchestration. Agent for mechanical multi-file layers. Edit for surgery, with Behavior dominant.</p>
<p><strong>End with Verify, and keep it.</strong> Specific PASS/FAIL checks derived from your restrictions. Re-run it after every change.</p>
<p><strong>Commit the prompts.</strong> They&#39;re not notes. They&#39;re the architecture record.</p>
<hr>
<p>The repo is at <a href="https://github.com/architect4hire/plannerpro" target="_blank" rel="noopener noreferrer">github.com/architect4hire/plannerpro</a>, prompts and all. Read the prompts document before the code — it&#39;s the better half of the repository, and it will make the code make sense in a way the code alone can&#39;t.</p>
<hr>
<p><em>Robert Felkins is the principal architect at Architect4Hire and the author of the upcoming book</em> Pratical AI Assisted Development, <em>which covers the full SCRUB system — tiered restrictions, layered generation, Edit Mode patterns, prompt chaining, and applying the framework to codebases you inherited rather than started.</em></p>
<p><em>And if your team is staring at a version of the three-boards problem — tooling, architecture, or an AI-assisted delivery practice that isn&#39;t paying for itself yet — that&#39;s the work I do. I take on fractional architecture engagements: a few days a month of senior architectural judgment without the full-time price tag, covering .NET and Azure platform work, team enablement, and AI-assisted development practices that survive contact with production. Reach out at <a href="mailto:robert@architect4hire.com">robert@architect4hire.com</a>.</em></p>
]]></content:encoded>
        </item>
        <item>
            <title>The Prompt Is the Plan: How I Built a 1,200-Line Angular Component With a Framework You&apos;ve Never Heard Of</title>
            <link>https://www.architect4hire.com/blog/the-prompt-is-the-plan-how-i-built-a-1-200-line-angular-component-with-a-framework-youve-never-heard-of</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/the-prompt-is-the-plan-how-i-built-a-1-200-line-angular-component-with-a-framework-youve-never-heard-of</guid>
            <pubDate>Thu, 04 Jun 2026 13:05:53 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>SCRUB Framework</category>
            <description>The AI does something genuinely terrifying: it writes code that runs fine, ships, and is quietly wrong. So I stopped typing instructions at it and started engineering them — and built a 1,200-line Angular component from a prompt document written before a single line of TypeScript.</description>
            <content:encoded><![CDATA[<p>Or: what happens when you stop typing instructions at the AI and start engineering them*</p>
<p>TL;DR - wanna check out the framework and sample application I built, grab the code at <a href="https://github.com/Architect4Hire/BrandForge" target="_blank" rel="noopener noreferrer">https://github.com/architect4hire/brandforge</a></p>
<hr>
<p>I want to tell you about a bug.</p>
<p>Not a dramatic one. No crash. No stack trace. The kind that&#39;s much worse than that — the kind where everything runs fine, the feature ships, and then three weeks later an operator notices that every single content card saved from the pipeline has the wrong social media copy attached to it.</p>
<p>Instagram post? Generic. TikTok hook? Missing. Pinterest description? Something the AI generated from the image name, not the actual image. The copy was there. It just belonged to the wrong image, from an earlier step in the workflow, because somewhere in twelve hundred lines of Angular TypeScript the social copy generation fired at the wrong moment. Before the operator had chosen an image. Before the system knew which image to describe.</p>
<p>The code compiled. The tests passed. The component ran. Nobody knew anything was wrong until they looked at the actual board cards.</p>
<p>That bug — or rather, the specific thing I did to prevent it from ever happening — is what this post is about.</p>
<hr>
<h2 id="the-ai-does-something-i-find-genuinely-terrifying">The AI Does Something I Find Genuinely Terrifying</h2>
<p>Before I get to the solution, I want to name the problem properly, because most people I talk to about AI-assisted development haven&#39;t quite articulated it yet.</p>
<p>The problem isn&#39;t that the AI gets things wrong. I can handle wrong. Wrong throws an error, wrong breaks a test, wrong crashes the app and puts a red line in the console. I can work with wrong.</p>
<p>The problem is that the AI gets things <em>plausibly right</em>.</p>
<p>Here&#39;s what that looks like in practice. You paste a task into Claude — build me a service, write me a component, add a method to this class — and the AI produces twelve hundred lines that are idiomatic. They reference the right services. The types are correct. The naming follows your conventions. The structure looks exactly like what a senior developer would write. You do a quick scan, it looks good, you run it, it runs. You ship it.</p>
<p>And then three weeks later you find out that the component has been defaulting the tip amount to twenty percent, or storing a PII field it shouldn&#39;t be touching, or firing an expensive AI call before the user has confirmed their intent, or — in my case — generating social copy off the wrong image because the ordering of two async calls was off by half a step.</p>
<p>I started calling this the Plausibility Trap. The output is so plausible-looking that you don&#39;t examine it carefully enough to catch the domain error buried inside it. A junior developer would produce code that&#39;s obviously wrong and you&#39;d catch it in review. The AI produces code that&#39;s only <em>subtly</em> wrong and you catch it in production.</p>
<p>The reason this happens is structural, not a flaw in the AI. The model is trained to produce idiomatic, coherent, conventionally-structured code. It has no idea what your product is actually supposed to do. It does not know that social copy should fire after image selection and not before. It doesn&#39;t know that your food blogger operators are going to be looking at that copy on a desktop CMS and not a mobile device, and that the UX decisions that flow from that distinction are significant. It doesn&#39;t know that your company had a legal incident around default values in financial fields. It knows how to write code. It does not know your domain.</p>
<p>So when you hand it a vague prompt and it fills in the gaps, it fills them with plausible defaults from training data. And the defaults are usually fine. Until they&#39;re not.</p>
<hr>
<h2 id="i-got-mad-about-this-and-built-a-framework">I Got Mad About This and Built a Framework</h2>
<p>I have been building software with AI tooling since before it was cool and after it got annoying. For a long time my standard approach was &quot;write a really long prompt and hope for the best.&quot; Which worked often enough to feel like a strategy, and failed spectacularly often enough that I spent a lot of time in debug mode wondering which part of my fifty-line prompt the AI had decided to ignore.</p>
<p>The thing I eventually figured out — the thing that changed how I work — is that the prompt failures are not random. They fall into patterns. And once you can see the pattern, you can address it systematically before you hit submit.</p>
<p>That systematic approach turned into a framework I call SCRUB.</p>
<p>It&#39;s not a brand new category of idea. It&#39;s a checklist. Five elements, every prompt. It forces you to address the five categories of AI-generated code failure before the code gets generated. I wrote a whole book about it — <em>Practical AI Assisted Development</em>, which covers the full framework plus a lot of the tooling philosophy around it. But the heart of it is five letters, and you can use it today, right now, without reading anything else I&#39;ve written.</p>
<p>Let me explain what each one means, and why the one most people skip is the most important.</p>
<hr>
<h2 id="what-scrub-actually-is">What SCRUB Actually Is</h2>
<p><strong>S is Scope.</strong> What are you asking for, named precisely. Not &quot;build me a service&quot; but &quot;build me exactly these four methods, with these exact signatures, in this file, and nothing else.&quot; The AI will expand your Scope if you let it. It will add helper methods you didn&#39;t ask for. It will create a base class when you just needed a concrete class. It will refactor adjacent code while it&#39;s in there because the adjacent code could be cleaner and the AI finds mess uncomfortable. Scope is the fence around what you actually want. Build it tight.</p>
<p><strong>C is Constraints.</strong> The technical rules your output must respect. Framework version. Naming conventions. Architectural patterns. DI style. Which async pattern you use. The AI doesn&#39;t know you&#39;re on Angular 20 standalone with signals and <code>@if</code>/<code>@for</code> control flow unless you say so. If you don&#39;t say so, you&#39;ll get <code>*ngFor</code> in your template and a <code>BehaviorSubject</code> in your class because those are the idioms from the version of Angular that&#39;s most represented in training data. I&#39;ve rebuilt the same component three times before I started writing constraints explicitly. I&#39;ve been on Angular 20 with signals for months now. I still write the constraint every time. The AI does not have session memory that says &quot;this person uses signal-based state management, use that.&quot;</p>
<p><strong>R is Restrictions.</strong> This is the one everyone skips, and it is the single most important element in the entire framework. Pay attention here.</p>
<p>Restrictions are the things you explicitly do not want. Not what you want built, what you want avoided. This is where you prevent the Plausibility Trap, because the Plausibility Trap lives inside the gap between what you asked for and what you said was wrong.</p>
<p>Here&#39;s the example from the book that sticks with me. Imagine you&#39;re building a tip calculator UI. You write a great Scope block, solid Constraints, the AI produces a beautiful component. Clean code. Nice UI. Runs great. The default tip is twenty percent.</p>
<p>You didn&#39;t specify the default. The AI picked twenty percent because twenty percent is the most common default in tip calculator UX in training data. Your company had a conversation six months ago about defaulting to fifteen percent because the restaurant group you work with operates in a market where twenty percent feels presumptuous and you got customer complaints. That decision is not in your codebase. It&#39;s not in any document the AI can read. It&#39;s in a Slack thread from March.</p>
<p>The Restriction is: &quot;Do NOT default the tip percentage to any value — the field should start empty and require explicit operator input.&quot; One line. Specific. With a reason.</p>
<p>That&#39;s what Restrictions do. They close the gaps that Scope leaves open. They&#39;re the part of the prompt that says &quot;I know you&#39;ll make a reasonable-sounding decision here — here&#39;s what&#39;s actually required.&quot;</p>
<p>I tier my Restrictions:</p>
<ul>
<li><strong>CRITICAL</strong>: &quot;Do NOT default the tip&quot; — missing this produces wrong data in production, silently</li>
<li><strong>IMPORTANT</strong>: &quot;Do NOT add helper methods to this file&quot; — missing this creates tech debt I&#39;ll pay later</li>
<li><strong>PREFERRED</strong>: &quot;Do NOT abbreviate variable names&quot; — style nit, not worth blocking on</li>
</ul>
<p>The CRITICAL ones are the ones that prevent the Plausibility Trap. Write them first. Write them with reasons. The reason is not documentation fluff — it&#39;s what tells the AI why the seemingly-reasonable default is wrong for this domain.</p>
<p><strong>U is Usage.</strong> Who runs this, under what conditions. A component built for a food blogger operator on a desktop CMS is different from the same component built for a tablet-based POS during a morning rush, even if the API contracts are identical. Usage is usually three to five lines. It is not usually interesting to write. It shapes dozens of downstream decisions and you won&#39;t notice most of them unless the AI gets them wrong.</p>
<p><strong>B is Behavior.</strong> What must not change. This is the element that stars in Edit Mode prompts — &quot;I want you to add these two validation rules and change nothing else, method order preserved, XML doc comments byte-for-byte identical.&quot; In greenfield work it&#39;s often N/A. In layered greenfield work — building a complex component in sequential steps — it becomes &quot;all signals and computed values from Layer 1 are preserved unchanged,&quot; and it matters more than you think because the AI will cheerfully improve your earlier work if you don&#39;t explicitly tell it not to.</p>
<p>Five elements. The discipline is not the template — it&#39;s the habit of asking, before you hit submit, where is R? Because R is the one people skip. And R is what prevents the social copy from firing off the wrong image.</p>
<hr>
<h2 id="recipeforge-and-why-this-component-specifically-was-a-problem">RecipeForge, and Why This Component Specifically Was a Problem</h2>
<p>RecipeForge is a food blogger CMS. It runs on .NET 10 with Aspire 13.3 on the backend, Angular 20 standalone on the frontend. The idea is that a food blogger picks a channel — we have six of them: Cozy Kitchen, Baker&#39;s Bench, Quick Bites, Grill Master, Plant Forward, World Flavors — picks a posting day, and the system walks them through building a ready-to-post image and full platform-tailored social copy in one workflow. Image generation via Venice.ai. Prompt composition via OpenAI. Copy for seven platforms: Instagram, TikTok, Pinterest, Facebook, X, Threads, and a blog intro paragraph. One screen, start to finish, everything saved to a Digital Asset Manager and dropped onto a Kanban board.</p>
<p>The content pipeline component is the centerpiece of that workflow. When I sat down to build it, here&#39;s what I was looking at:</p>
<p>Nine discrete steps, represented as a TypeScript union: <code>&#39;config&#39; | &#39;seeding&#39; | &#39;reviewSeed&#39; | &#39;composing&#39; | &#39;reviewPrompt&#39; | &#39;rendering&#39; | &#39;pickImage&#39; | &#39;social&#39; | &#39;done&#39;</code>. Thirty-one signals. Six API calls — two to OpenAI, two to Venice.ai, one to the DAM, one to the Kanban service. Two deliberate human-in-the-loop checkpoints where the operator can intervene and edit before anything expensive fires. A variant grid showing up to four generated images. A full-screen lightbox with keyboard navigation. Drag-and-drop reference image upload with a &quot;reverse-engineer this image into a prompt&quot; button. State that cleans itself up completely between runs.</p>
<p>That&#39;s not a complex component. That&#39;s a complex workflow expressed as a component. There&#39;s a difference. A complex component has a lot of moving parts. A complex workflow has ordering constraints — real business rules about what happens before what, enforced by the UI, with consequences if the ordering is wrong.</p>
<p>The consequence here, as I explained at the top, is social copy that doesn&#39;t match the image. Because there are three plausible places in this workflow where you could call <code>runSocial()</code>, and two of them are wrong. The AI, given a loose prompt, will pick the one that looks most natural from a &quot;I&#39;m building a step machine&quot; perspective, which is &quot;fire social copy generation after the render step completes.&quot; Completely reasonable. Wrong. Social needs to fire after image selection, not after image rendering, because social copy has to be written to describe a specific image — the one the operator actually kept — not a batch that might include images they&#39;re about to discard.</p>
<p>If I one-shotted this component, I would have gotten that wrong. And I probably wouldn&#39;t have caught it for days.</p>
<hr>
<h2 id="the-document-i-wrote-before-i-wrote-a-single-line-of-typescript">The Document I Wrote Before I Wrote a Single Line of TypeScript</h2>
<p>There&#39;s a file in the RecipeForge repository at <code>docs/SCRUB-PIPELINE-PROMPTS.md</code>. It exists before the component exists. The prompts document is not documentation of what I did — it&#39;s the plan I worked from. The thing I reviewed and revised before I opened Claude Code Chat.</p>
<p>The document follows a four-step chain: Context → Plan → Execute → Verify.</p>
<p>Context gets the AI oriented to the codebase. Plan aligns on architecture before code exists. Execute generates the code in layers. Verify confirms the invariants held. The whole chain took about half a day to run. The component compiled cleanly at each layer.</p>
<p>Let me walk through what&#39;s in each step and why the decisions look the way they do.</p>
<hr>
<h2 id="pre-load-context-making-the-ai-read-before-it-writes">Pre-Load Context: Making the AI Read Before It Writes</h2>
<p>The very first thing in the document is a snippet I paste at the start of every Claude Code Chat session before doing anything else. It lists six files and tells the AI to read them:</p>
<ul>
<li><code>domain.models.ts</code> — channel keys, pick lists, social platform definitions, all the seed and response types</li>
<li><code>scene-presets.ts</code> — nineteen food photography scenes across four scene groups</li>
<li><code>workflow-api.service.ts</code> — every API method the component will call, with their actual TypeScript signatures</li>
<li><code>dam-api.service.ts</code> — the <code>saveAsset</code> method</li>
<li><code>kanban-api.service.ts</code> — the <code>createCard</code> method</li>
<li><code>image.component.ts</code> — an existing working component that already does a version of the seed-and-render flow</li>
</ul>
<p>That last one is pulling a lot of weight. The instruction doesn&#39;t say &quot;here are the patterns I want you to follow, let me describe them.&quot; It says &quot;here is a component that already does a version of this correctly, read it and match what you see.&quot; That is the most reliable Constraint you can write. Not a description of the convention — a pointer to an exemplar. The AI reads the actual code and extracts the pattern from it. Every time I&#39;ve tried to describe a pattern in prose versus pointing at a live example, the example wins. Not by a little. By a lot.</p>
<p>The last line of the pre-load is: &quot;Do not generate any code yet.&quot;</p>
<p>This matters because Claude will sometimes start generating the moment it finishes reading files. It&#39;s being helpful. I don&#39;t want helpful yet. I want understanding. &quot;Do not generate any code yet&quot; is four words that prevent a premature response and keep the session in the right state for the Context step.</p>
<hr>
<h2 id="step-1-context-ask-for-understanding-before-you-ask-for-code">Step 1 — Context: Ask for Understanding Before You Ask for Code</h2>
<p>The Context prompt asks Claude to produce a structured summary. Not code — understanding. What signal and computed patterns does <code>image.component.ts</code> use? What are the exact shapes of <code>ContentPromptSeedResult</code>, <code>ImageBatchResponse</code>, and <code>SocialWorkflowResponse</code>? Which <code>WorkflowApiService</code> methods correspond to which pipeline stages?</p>
<p>There are two reasons I do this.</p>
<p>One: if the AI misunderstood something, I catch it here while it&#39;s cheap. A misunderstanding in the Context step costs me a one-line correction. A misunderstanding in Layer 4 costs me a template rewrite.</p>
<p>Two: asking the model to synthesize what it read before it produces new code improves the quality of the production step. I know this sounds like folk magic. It isn&#39;t. The model has to build an active representation of the relevant patterns before generating against them, and making that step explicit and separable — as a distinct prompt with a distinct deliverable — produces better output than jumping straight to execution. I noticed this pattern after running maybe thirty layered prompt chains. I tested it by removing the Context step and comparing results. The Context step wins.</p>
<p>I&#39;ve caught two real errors in RecipeForge&#39;s Context step — both about how <code>firstValueFrom()</code> was being used in the existing service layer. Both would have produced subtle async bugs in the methods layer if they&#39;d made it to Layer 6. One correction in prose, at the Context step, and neither showed up again.</p>
<hr>
<h2 id="step-2-plan-the-architecture-review-before-the-build">Step 2 — Plan: The Architecture Review Before the Build</h2>
<p>The Plan prompt asks for a prose proposal. The step union type. The thirty-one signals — which ones and what they represent. The two edit checkpoints and what state the operator needs editable at each. How the lightbox integrates with batch state. How social copy fits into the post-pick flow. All in prose, zero TypeScript.</p>
<p>This is the step everyone skips. I understand the impulse. You have the Context, you know what you want, just generate the code already. Skipping the Plan is how you get clean code that does the wrong thing.</p>
<p>A bad plan generates plausible code. A good plan generates code where the only surprises are in the formatting.</p>
<p>What I&#39;m specifically probing in the Plan review: does the AI agree that social fires after image selection? Does it understand that <code>reviewSeed</code> and <code>reviewPrompt</code> are active edit windows and not loading spinners? Does it know that the variant discard happens inside <code>selectImage()</code>, not in a separate cleanup step? These are the decisions with ordering consequences. If the plan is wrong about any of them, the Execute prompts will generate wrong code from a correct-looking starting point.</p>
<p>I push back in prose when I disagree with the proposal. Sometimes I don&#39;t push back — the AI&#39;s proposal is better than my initial instinct, and I update the prompts document before running Execute. This has happened twice on RecipeForge. The AI suggested a derived computed signal for the visible-platforms list that I hadn&#39;t thought of, and it was cleaner than what I had planned. The Plan step is not &quot;convince the AI to do what I decided&quot; — it&#39;s alignment. It takes maybe fifteen minutes. On a component this size, it is the best-spent fifteen minutes in the whole build.</p>
<hr>
<h2 id="execute-seven-layers-each-one-verifiable">Execute: Seven Layers, Each One Verifiable</h2>
<p>Execute is where the code gets built. Seven prompts, each one scoped to a piece of the component that can be checked independently, each one explicitly preserving what came before.</p>
<h3 id="layer-1-the-signal-skeleton">Layer 1: The Signal Skeleton</h3>
<p>Layer 1 produces the class with all signals, computed derivations, constants, injected services, and read-only pick lists — and nothing else. The <code>@Component</code> decorator. An empty template. No method bodies. Every method a stub.</p>
<p>This is the most boring layer. It&#39;s also the most important one.</p>
<p>If the signal names are wrong in Layer 1, every subsequent layer references the wrong names and you get a cascade of TypeScript errors that trace back to a naming decision that would have taken two minutes to get right at the start. The goal is a clean compile from Layer 1 onward. Everything on top of a passing build, never in the dark.</p>
<p>The CRITICAL restrictions here are blunt: &quot;Do NOT write template HTML beyond the bare decorator&quot; and &quot;Do NOT write any method bodies.&quot; The AI will try to fill these in because a component class with no template and no implementation is unusual-looking. &quot;Helpful&quot; means filling in what appears missing. Helpful is not what I want here. I want a typed signal inventory. I&#39;ll take helpful in Layer 2.</p>
<p>The IMPORTANT restriction — &quot;no RxJS Subject or BehaviorSubject for component state&quot; — is there because Angular 20 with signals means exactly that. Signals for state, <code>firstValueFrom()</code> to bridge HTTP observables into async methods. Every time I&#39;ve run a pipeline-style prompt without this restriction, at least one BehaviorSubject appears. Not because the AI is doing something wrong — BehaviorSubject is a completely correct Angular state pattern, just from a slightly older era. Without the restriction, the training distribution wins.</p>
<h3 id="layers-2-through-5-building-the-template-in-slices">Layers 2 Through 5: Building the Template in Slices</h3>
<p>The template is four layers. Each one adds a vertical slice of the UI without touching what already exists.</p>
<p><strong>Layer 2</strong> is the page header and config card — channel selector, posting day, variant count, the Run Pipeline button. Unremarkable on its own. The important thing here is the Behavior block: &quot;All signals and computed values from Layer 1 are preserved unchanged.&quot; That one line stops the AI from quietly improving my signal initializations while it adds the template. Without it, you get a template that&#39;s fine and a class that&#39;s been silently refactored. I have spent an afternoon tracing that particular class of diff-confusion. Not fun.</p>
<p><strong>Layer 3</strong> adds the mid-workflow steps: the seeding spinner, the reviewSeed edit block with scene selector and reference image upload zone, the composing spinner, the reviewPrompt block with the Venice prompt textarea and the character counter. These are the two human-in-the-loop checkpoints. This is where the operator can intervene.</p>
<p>One restriction from this layer I want to call out because it&#39;s a good illustration of how Restrictions work in practice: &quot;Do NOT show the character counter outside the reviewPrompt step.&quot;</p>
<p>The first time I generated this template without that restriction, the counter appeared in the reviewSeed block, attached to the concept text field. Which makes a certain template-generation logic — it&#39;s a textarea, there&#39;s a counter component nearby, they belong together. What the AI doesn&#39;t know is that concept text has no character limit, and showing a counter on a field with no limit creates visual anxiety the operator doesn&#39;t need at that stage of the workflow. They&#39;re not trying to stay under a character budget there. They&#39;re editing a concept description. The counter is noise.</p>
<p>That&#39;s domain knowledge. The AI has no way to derive it from the code. The only way it ends up right is if I put it in the Restriction. I put it in the Restriction. Never saw the problem again.</p>
<p><strong>Layer 4</strong> is the variant grid and the lightbox overlay. This layer has what I consider the most important CRITICAL restriction in the entire prompts document:</p>
<p>&quot;Do NOT allow the Keep button inside the lightbox to call <code>saveAll()</code> — it must call <code>selectFromLightbox()</code>, which chains to <code>selectImage()</code>, which chains to <code>runSocial()</code>.&quot;</p>
<p>Let me explain why this is CRITICAL and not just IMPORTANT.</p>
<p>The lightbox Keep button is the moment the operator says &quot;this is the image I want.&quot; When they click Keep, three things need to happen in sequence: the selection gets recorded, the unchosen variants get discarded, and the social copy gets generated based on the selected image. That entire sequence lives inside <code>selectImage()</code>.</p>
<p>If the Keep button calls <code>saveAll()</code> instead, the operator is saving a content card before the social copy has been generated. The DAM asset gets created. The Kanban card gets dropped. Both of them have empty or stale social copy fields because the social workflow hasn&#39;t run yet. No error. No warning. Just a board full of cards with wrong descriptions that an operator discovers weeks later.</p>
<p>Three words in a button binding. One CRITICAL restriction. The difference between a bug that&#39;s embarrassing to discover and a bug that&#39;s expensive to fix.</p>
<p><strong>Layer 5</strong> is the result grid — chosen image on the left, all seven platform copy blocks on the right, save bar below. The save bar has the platform selector, card title, scheduled date, and the Save button. The CRITICAL restriction: &quot;Do NOT show the Save button as enabled while <code>socialState() !== &#39;success&#39;</code>.&quot;</p>
<p>The operator cannot save until social copy generation has succeeded. That&#39;s not a UX detail — that&#39;s the product rule. Without the restriction, the AI produces a Save button that&#39;s enabled as soon as the step renders, because that&#39;s what most forms in training data look like. Forms have submit buttons and submit buttons are available. The contextually-gated submit button requires the explicit rule.</p>
<h3 id="layer-6-the-methods-layer">Layer 6: The Methods Layer</h3>
<p>Layer 6 drops method bodies into all the stubs Layer 1 created. <code>run()</code>, <code>continueFromSeed()</code>, <code>renderImage()</code>, <code>selectImage()</code>, <code>runSocial()</code>, <code>saveAll()</code>, the reference image handlers, the lightbox helpers, the copy button, the restart flow. Every orchestration method.</p>
<p>I put this layer last deliberately. The entire template exists before the method bodies are written. When the AI is building <code>selectImage()</code>, it can see the full template context it&#39;s operating in — it knows what the lightbox looks like, it knows what the social grid expects, it knows what the save bar requires. It&#39;s not writing methods in a vacuum and hoping the template that comes later matches.</p>
<p>The sequencing restrictions in this layer are the ones I care most about:</p>
<p>&quot;Do NOT call <code>runSocial()</code> before <code>selectImage()</code> resolves — social fires inside <code>selectImage()</code> only.&quot; CRITICAL because the violation produces silent wrong data. Social firing too early generates copy that describes the wrong image. No exception. No test failure. Just bad content on your board.</p>
<p>&quot;Do NOT delete the chosen image variant — delete only the unchosen variants.&quot; This one seems obvious until you&#39;re reading auto-generated code and find <code>.forEach(f =&gt; api.deleteImage(f).subscribe())</code> iterating over all the filenames, including the one the operator just kept. The AI is pattern-matching on &quot;clean up after image selection&quot; from training data. The correct behavior requires saying it explicitly, because &quot;delete all but one&quot; is a restriction, not a convention.</p>
<p>The Constraint for async style: &quot;All orchestration methods use <code>async/await</code> with <code>firstValueFrom()</code> — no <code>.subscribe()</code> inside orchestration logic.&quot; The reason is error handling. Async/await with try/catch in an orchestration method gives you predictable recovery — you know which step failed, you set the right error signal, you return the UI to the right state. A <code>.subscribe()</code> in the middle of an orchestration chain makes error recovery harder to reason about and harder to test. The rule isn&#39;t theoretical. Every time I&#39;ve let a <code>.subscribe()</code> sneak into orchestration code, I&#39;ve regretted it.</p>
<h3 id="layer-7-helpers-and-styles">Layer 7: Helpers and Styles</h3>
<p>Layer 7 adds the private derivation helpers and the full component CSS. Smaller than what came before, but <code>pickLocation()</code> is worth a mention because it&#39;s a clean example of domain knowledge that has to live in the prompt.</p>
<p><code>pickLocation()</code> maps cooking method keywords to scene presets — grilling and smoking go to outdoor scenes with a grill-adjacent preference, baking goes to kitchen scenes with a rustic preference, no-cook and raw-prep go to table setups. The function is maybe twenty lines. The mapping inside it represents product decisions we made about how food photography should relate to cooking context. Without those decisions in the Restriction block, the AI produces either a random mapping or a plausible-looking one based on whatever associations are strongest in training data.</p>
<p>Writing the mapping down in the prompt is also writing it down as product documentation. Anyone who reads the prompts file six months from now understands not just how the function works but why it maps the way it does.</p>
<hr>
<h2 id="the-verify-step-thirteen-specific-checks">The Verify Step: Thirteen Specific Checks</h2>
<p>After all seven layers compile, there&#39;s a Verify prompt. It asks Claude to re-read the completed component and run thirteen specific checks, reporting each as PASS or FAIL.</p>
<p>The CRITICAL checks hit the exact failure modes I was most worried about:</p>
<p>Does any step transition skip a review checkpoint? Does <code>runSocial()</code> fire anywhere except inside <code>selectImage()</code>? Does <code>selectImage()</code> accidentally delete the chosen filename? Does <code>saveAll()</code> run while <code>socialState() !== &#39;success&#39;</code>? Is there any <code>localStorage</code>, <code>sessionStorage</code>, or <code>document.cookie</code> anywhere in the file? Does any orchestration method use <code>.subscribe()</code> instead of <code>firstValueFrom()</code>? Are all seven social platforms rendered in the result grid? Is the Venice prompt character limit enforced at 1499 characters? Is there any <code>*ngIf</code> or <code>*ngFor</code> in the template? Does the lightbox Keep button call <code>saveAll()</code>?</p>
<p>That last one is on the list specifically because in an early iteration, before I had the CRITICAL restriction nailed down, the Keep button did call <code>saveAll()</code>. I caught it in the Verify step, not the diff review, because I was scanning the diff too fast. The Verify step is there precisely for that situation — the one where you&#39;re moving quickly and you miss the thing that matters.</p>
<p>Thirteen checks. Roughly half a day of build time. Zero production incidents related to ordering bugs on this component.</p>
<hr>
<h2 id="what-i-would-do-differently">What I Would Do Differently</h2>
<p>A few things.</p>
<p>I would write the Restrictions for each layer before writing the Scope. My current habit is still backward sometimes — I write what I want built, then I think about what could go wrong. The right habit is Restrictions-first. Sit down with a blank [R] block and ask &quot;what are the five most plausible ways this layer could compile and still be wrong?&quot; The list is almost always longer than five, but asking the question surfaces the critical ones before they bite you in production.</p>
<p>I&#39;d be more explicit about CSS variable usage in Layer 7. I had a restriction that said &quot;no hardcoded color values except platform dot colors,&quot; and the first pass had hex values showing up in animation keyframes and hover transitions. The restriction was right but not specific enough. Lesson filed: restrictions on styles should name specific properties, not just categories.</p>
<p>I would add a Known Limitations section to the prompts document earlier. I added it at the end. During the build I was tracking four edge cases in my head — the reference image <code>.subscribe()</code> pattern, the <code>BRAND_ARCHETYPES</code> dependency, the lightbox containing-block CSS caveat, the orphaned variants on navigation. Writing them down as I encountered them would have saved me the mental overhead of holding them until the build was done. The prompts document is not just a pre-build artifact. It&#39;s a living document you update as you learn things.</p>
<hr>
<h2 id="why-this-matters-more-than-it-looks-like-it-does">Why This Matters More Than It Looks Like It Does</h2>
<p>I want to be clear about what the SCRUB prompts document actually is, because I&#39;ve seen people mistake it for something else.</p>
<p>It is not documentation. It&#39;s not a write-up of what I did after the fact. It is the plan I worked from before I generated a single line of code for this component. And because it exists as a versioned, committed file in the repository, it becomes the first place I look when the component needs to change, not the last.</p>
<p>Buffer integration is on the roadmap — a &quot;Schedule in Buffer&quot; button that pushes copy and images directly to Buffer from the pipeline save step. When I add that feature, I will update <code>SCRUB-PIPELINE-PROMPTS.md</code> first. The new Layer 5 restrictions, the new Layer 6 method contracts, the additional Verify check for the Buffer call. Then I run the updated prompts. The component is built and maintained with the same discipline.</p>
<p>This is what the framework means when it says to treat prompts like code. Not a metaphor. A literal practice. The prompts directory is in source control. The prompts have authors and commit history. When something goes wrong in the component — and something always goes wrong eventually — the prompts tell me exactly which element of which layer broke.</p>
<p>Wrong default? Missing Restriction. Wrong state transition? Vague Scope or missing Restriction. Broke something I didn&#39;t touch? Weak Behavior block. The diagnostic card maps symptom to element. Element to fix. No hunting.</p>
<p>The other thing the prompts document gives me is a record of why the code is the way it is. Six months from now, when someone new is reading <code>pipeline.component.ts</code> and wondering why <code>runSocial()</code> only ever fires inside <code>selectImage()</code> and not in any of the three other plausible locations — the answer is in the prompts file. Not buried in a comment. Not in my head. In a file that&#39;s been reviewed, committed, and is available to anyone who runs <code>git log</code>.</p>
<p>That is worth the fifteen minutes it takes to write the plan before you write the code. Every time.</p>
<hr>
<h2 id="the-practical-version">The Practical Version</h2>
<p>If you want to try this on a component you&#39;re building:</p>
<p><strong>Start with Pre-load Context.</strong> Name the files the AI needs to read. Tell it explicitly not to generate anything yet. Five minutes, not fifty.</p>
<p><strong>Run a Context step first.</strong> Ask for a summary of what it read before asking for code. Read the summary. Correct misunderstandings in prose. This is the cheapest error-catching stage in the whole chain.</p>
<p><strong>Write a Plan prompt and read the proposal.</strong> Push back on anything with wrong ordering or wrong behavior. Do not skip this step because it feels slow. It is the cheapest insurance in the whole process.</p>
<p><strong>Split Execute into layers.</strong> Four to seven is usually right. If you can&#39;t describe what a layer produces in three sentences, the layer is too big. Split it.</p>
<p><strong>Write Restrictions before Scope.</strong> Ask yourself: what are the five ways this layer could compile and still be wrong? Write those down first. The Scope block is usually easier.</p>
<p><strong>Tier your Restrictions.</strong> CRITICAL means silent wrong data in production. IMPORTANT means technical debt you&#39;ll pay. PREFERRED means style. Know which ones you can compromise on and which ones you absolutely cannot.</p>
<p><strong>Write a Verify prompt.</strong> Not &quot;does it run&quot; — specifically, does it have the behaviors you required? Thirteen specific PASS/FAIL checks on a twelve-hundred-line component caught a real bug I missed in review.</p>
<p><strong>Commit the prompts document alongside the code.</strong> Update it when the component changes. It is not overhead. It is the architectural record of the component.</p>
<hr>
<p>The <a href="https://github.com/Architect4Hire/BrandForge/docs/SCRUB-PIPELINE-PROMPTS.md" target="_blank" rel="noopener noreferrer"><code>docs/SCRUB-PIPELINE-PROMPTS.md</code></a> file is in the RecipeForge repository if you want to read the actual prompts rather than my description of them. The component it produces — <code>brandforge-ui/src/app/features/pipeline/pipeline.component.ts</code> — is the result of running those prompts in sequence with the pre-loaded context. Clean compile at every layer. All thirteen Verify checks passed on first run.</p>
<p>That is what disciplined prompt engineering looks like. Not faster, necessarily. Not more impressive. Just fewer bugs that compile and run and don&#39;t get caught until production. And when something does go wrong, a clear map back to the decision that produced it.</p>
<p>That&#39;s a trade I&#39;ll take every time.</p>
<hr>
<p>*Robert Felkins is the principal architect at Architect4Hire and the author of Practical AI Assisted Development. If you want to go deeper on SCRUB — the full tiered restriction system, the Edit Mode prompt patterns, applying the framework to existing codebases — the book covers all of it. Find it at the companion repository, or reach out at <a href="mailto:robert@architect4hire.com">robert@architect4hire.com</a>.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Behind the Scenes: Why AspireHire’s UI Started with an Angular + Bootstrap Template</title>
            <link>https://www.architect4hire.com/blog/behind-the-scenes-why-aspirehires-ui-started-with-an-angular-bootstrap-template</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/behind-the-scenes-why-aspirehires-ui-started-with-an-angular-bootstrap-template</guid>
            <pubDate>Tue, 02 Sep 2025 11:45:55 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Building AspireHire</category>
            <description>The backend came together in five minutes: ASP.NET Core 9, Aspire, Azure SQL, microservices. The UI was a different beast entirely — and buying a template instead of building one was the right architectural call.</description>
            <content:encoded><![CDATA[<p><strong>The UI Crossroads</strong></p>
<p>When I first mapped out AspireHire’s architecture, the backend came together fast. ASP.NET Core 9, .NET Aspire for orchestration, Azure SQL and Cosmos for persistence, and microservices for Profiles, Jobs, Recruiters, Contracts, Messaging. That’s my comfort zone. I could sketch it out in five minutes and feel good about it.</p>
<p>The UI, though? That was a different beast.</p>
<p>We needed something recruiters would trust and developers would enjoy using. Clean, modern, responsive. But here’s the thing—I’m not a front-end developer. I’m a backend architect. My brain runs on APIs, services, queues, and data models. Drop me into a blank Angular project and I’ll be muttering at flexbox for days.</p>
<p>So I had a decision to make.</p>
<p>Do I build the UI from scratch and spend weeks wrangling layout and styling? Or do I start with a polished Angular + Bootstrap template and get to value faster?</p>
<p>I chose the template. That one move saved us months and gave AspireHire a head start.</p>
<p><strong>Why Angular Made Sense</strong></p>
<p>Let’s talk about the framework choice first.</p>
<ul>
<li>Angular has a strong structure and built-in TypeScript support. That’s great for long-term maintainability.</li>
<li>The ecosystem is deep. Charts, grids, auth helpers, translation tools—you name it, Angular has it.</li>
<li>It scales with the team. As AspireHire grows, we’ll bring in front-end devs who already know Angular. It’s a safe bet.</li>
<li>It works well with .NET. Angular and ASP.NET Core integrate smoothly when it comes to routing, authentication, and API calls.</li>
</ul>
<p>So Angular was the right fit. The next question was how to get a usable UI up and running quickly.</p>
<p><strong>Why Bootstrap Instead of Angular Material</strong></p>
<p>Angular Material is solid, but it comes with a strong Google design identity. AspireHire needed something more flexible and easier to brand.</p>
<p>Bootstrap gave us that.</p>
<ul>
<li>It’s familiar. Buttons, grids, and navbars look exactly how users expect.</li>
<li>It’s easy to customize. We could rebrand it with our own colors and typography without a fight.</li>
<li>It’s mobile-first. The grid system is responsive and well-tested.</li>
<li>The community is huge. Tons of examples, documentation, and support out there.</li>
</ul>
<p>Bootstrap gave us a clean, professional starting point that didn’t feel like a cookie-cutter template. And we could make it our own with just a few tweaks.</p>
<p><strong>Why I Didn’t Build It From Scratch</strong></p>
<p>If I’d gone the hand-crafted route, here’s what I would’ve been dealing with:</p>
<ul>
<li>Setting up Angular project scaffolding</li>
<li>Building layouts, navbars, and grid systems from scratch</li>
<li>Writing custom components for buttons, forms, tables, and modals</li>
<li>Debugging CSS across browsers</li>
<li>Wrestling with flexbox to get mobile breakpoints right</li>
</ul>
<p>That’s a lot of time before AspireHire even looks like an app. Meanwhile, stakeholders are asking when they’ll see something real.</p>
<p>Starting from scratch would’ve slowed us down and risked losing momentum.</p>
<p><strong>What the Template Delivered</strong></p>
<p>By choosing a solid Angular + Bootstrap template, we got:</p>
<ul>
<li>Clean layouts with dashboards, sidebars, and navbars already styled</li>
<li>Reusable components like buttons, forms, modals, and tables</li>
<li>Auth flows including login, signup, and password reset screens</li>
<li>A responsive grid system that worked across devices</li>
<li>Easy branding options to match AspireHire’s style</li>
</ul>
<p>Instead of building a design system from the ground up, we could plug in our backend APIs and start delivering value.</p>
<p><strong>How It Fit with .NET Aspire</strong></p>
<p>This wasn’t just about saving time on the front end. It was about making sure everything lined up with our backend architecture.</p>
<p>Here’s how it worked:</p>
<ol>
<li>A user logs in using a Bootstrap-styled form</li>
<li>Angular services call our .NET Identity microservice</li>
<li>JWT tokens come back and get stored in Angular</li>
<li>Angular interceptors attach those tokens to API calls</li>
<li>Microservices respond with JSON for profiles, jobs, recruiters, and contracts</li>
<li>Bootstrap components display the data in clean, responsive layouts</li>
</ol>
<p>The UI felt polished and professional. The backend stayed scalable and observable. It was a smooth handoff between layers.</p>
<p><strong>Branding with Bootstrap</strong></p>
<p>Out of the box, Bootstrap is clean but generic. AspireHire needed a little more personality.</p>
<p>So we customized:</p>
<ul>
<li>Updated the color palette with warm oranges, deep reds, vibrant purples, and balanced blues</li>
<li>Swapped in Google Fonts to match our brand voice—professional and approachable</li>
<li>Dropped in AspireHire logos across headers, footers, and login screens</li>
<li>Tweaked SCSS variables to give the UI a distinct look while keeping Bootstrap’s structure</li>
</ul>
<p>In just a few days, the UI looked like AspireHire. Not just another Bootstrap site.</p>
<p><strong>What I Learned Along the Way</strong></p>
<p>Templates are helpful, but they’re not magic. Here are a few takeaways:</p>
<ul>
<li>Learn the template’s structure. Even if you’re not a front-end expert, understand how routing, services, and components are wired.</li>
<li>Don’t over-customize too early. Focus on delivering business value first. Styling can come later.</li>
<li>Use Bootstrap’s utilities. The spacing, grid, and responsive helpers are there for a reason. Don’t reinvent them.</li>
<li>Keep your integration clean. Angular services should wrap backend APIs without adding business logic to the UI.</li>
<li>Plan for growth. Bootstrap is a great starting point, but as AspireHire scales, we’ll likely build a design system on top of it.</li>
</ul>
<p><strong>Why This Was a Strategic Move</strong></p>
<p>This wasn’t about cutting corners. It was about making smart choices.</p>
<ul>
<li>We got to demos faster. Stakeholders saw real screens within two weeks.</li>
<li>We saved time and money. Less boilerplate meant more focus on features.</li>
<li>We stayed focused. Developers worked on what mattered—like recruiter-matching algorithms and developer portfolios.</li>
<li>We built a foundation. Bootstrap’s familiar patterns make it easy for future hires to jump in and contribute.</li>
</ul>
<p>The template wasn’t a shortcut. It was leverage.</p>
<p><strong>Wrapping It Up</strong></p>
<p>AspireHire’s goal is to help developers showcase their skills. That meant we needed to get a working, branded UI in front of users quickly.</p>
<p>The Angular + Bootstrap template gave us that launchpad. It let me stay focused on backend architecture while still delivering a front end that looked and felt like AspireHire.</p>
<p>So if you’re a backend-first developer staring down a UI build, here’s my advice. Don’t get stuck trying to hand-roll every pixel. Use a template. Bootstrap it. Ship faster. And spend your energy where it counts.</p>
<p>Because at the end of the day, nobody’s impressed that you built a navbar from scratch. They care that your platform works, solves real problems, and feels trustworthy.</p>
<p>AspireHire is living proof that smart shortcuts lead to better outcomes.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Why We Built an Audit Trail into AspireHire (And Why You Should Care)</title>
            <link>https://www.architect4hire.com/blog/why-we-built-an-audit-trail-into-aspirehire-and-why-you-should-care</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/why-we-built-an-audit-trail-into-aspirehire-and-why-you-should-care</guid>
            <pubDate>Sun, 31 Aug 2025 16:28:16 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Building AspireHire</category>
            <description>&quot;Audit framework&quot; sounds about as exciting as watching paint dry. Then comes the &quot;oh shit&quot; moment where someone asks who changed a record and when — and you find you cannot answer. Here is what we built instead.</description>
            <content:encoded><![CDATA[<p><em>Or: How I Learned to Stop Worrying and Love Data Accountability</em></p>
<p>Hey folks! So, I&#39;ve been deep in the trenches building AspireHire lately, and I wanted to share something that might not seem sexy on the surface but is absolutely crucial for any serious application: our audit framework. Yeah, I know, &quot;audit framework&quot; sounds about as exciting as watching paint dry, but stick with me here—this stuff matters more than you think.</p>
<h2 id="the-and-quot-oh-shit-and-quot-moment-that-started-it-all">The &quot;Oh Shit&quot; Moment That Started It All</h2>
<p>Picture this: You&#39;re running a job platform where candidates are uploading resumes, employers are posting positions, and money is changing hands. Then one day, someone calls and says, &quot;Hey, my job posting disappeared, and I need to know exactly what happened and when.&quot;</p>
<p>Without an audit trail? You&#39;re basically playing detective with a blindfold on. With one? You can tell them exactly who did what, when they did it, and what changed. That&#39;s the difference between looking professional and looking like you&#39;re running your platform out of a garage (even if you literally are).</p>
<h2 id="what-we-actually-built">What We Actually Built</h2>
<p>Instead of reinventing the wheel or bolting on some third-party solution that would cost us an arm and a leg, we built our own <code>AuditDBContext</code> that sits right on top of Entity Framework Core. It&#39;s elegant, it&#39;s automatic, and it just works.</p>
<p>Here&#39;s the beautiful part, every time someone saves changes to our database, our audit framework automatically kicks in and says, &quot;Hold up, let me just record what&#39;s happening here.&quot; It captures:</p>
<ul>
<li><strong>What entity changed</strong> (User profile? Job posting? Application?)</li>
<li><strong>What type of action</strong> (Created, updated, deleted)</li>
<li><strong>The exact fields that changed</strong> (Old value vs. new value)</li>
<li><strong>When it happened</strong> (Down to the millisecond)</li>
</ul>
<p>The magic happens in our <code>SaveChangesAsync</code> override:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task&lt;<span class="hljs-built_in">int</span>&gt; <span class="hljs-title">SaveChangesAsync</span>(<span class="hljs-params">CancellationToken cancellationToken = <span class="hljs-literal">default</span></span>)</span>
{
    <span class="hljs-keyword">var</span> entityEntries = ChangeTracker.Entries()
        .Where(x =&gt; x.State != EntityState.Unchanged &amp;&amp; x.State != EntityState.Detached);

    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> entry <span class="hljs-keyword">in</span> entityEntries)
    {
        <span class="hljs-keyword">var</span> audit = <span class="hljs-keyword">new</span> Audit
        {
            TableName = entry.Entity.GetType().Name,
            Action = entry.State.ToString(),
            KeyValues = GetKeyValues(entry),
            Changes = GetFieldChanges(entry) != <span class="hljs-literal">null</span> ? System.Text.Json.JsonSerializer.Serialize(GetFieldChanges(entry)) : <span class="hljs-literal">null</span>
        };
        Audits.Add(audit);
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">base</span>.SaveChangesAsync(cancellationToken);
}</code></pre>
<p>Zero extra code in our business logic. Zero chance of forgetting to log something. It just happens automatically, like a good DevOps pipeline.</p>
<h2 id="real-talk-why-this-matters-for-aspirehire">Real Talk: Why This Matters for AspireHire</h2>
<h3 id="trust-is-everything-in-hiring">Trust is Everything in Hiring</h3>
<p>When you&#39;re dealing with people&#39;s careers and livelihoods, trust isn&#39;t optional. Candidates need to know their data is safe. Employers need confidence that their job postings and candidate information are handled properly.</p>
<p>Having a complete audit trail means we can:</p>
<ul>
<li><strong>Prove compliance</strong> when auditors come knocking</li>
<li><strong>Investigate issues</strong> quickly and thoroughly</li>
<li><strong>Build trust</strong> by being transparent about data handling</li>
<li><strong>Prevent disputes</strong> by having the receipts</li>
</ul>
<h3 id="the-technical-benefits-are-real-too">The Technical Benefits Are Real Too</h3>
<p>Beyond the obvious compliance stuff, our audit framework gives us superpowers:</p>
<p><strong>Debugging Production Issues</strong>: Ever had a bug report that starts with &quot;Something weird happened last Tuesday&quot;? With full audit trails, we can reconstruct exactly what &quot;weird&quot; means.</p>
<p><strong>Analytics and Insights</strong>: Want to know how users actually behave on the platform? Audit data is a goldmine for understanding user patterns and optimizing UX.</p>
<p><strong>Data Recovery</strong>: When (not if) someone accidentally deletes something important, we can see exactly what was deleted and potentially restore it.</p>
<h2 id="the-implementation-that-actually-works">The Implementation That Actually Works</h2>
<p>Here&#39;s what I love about our approach—it&#39;s dead simple but incredibly powerful. We have three main pieces:</p>
<ol>
<li><strong>The <code>AuditDBContext</code></strong> - The orchestrator that captures everything</li>
<li><strong>The <code>Audit</code> entity</strong> - Stores the high-level change information</li>
<li><strong>The <code>AuditFieldChange</code> class</strong> - Captures field-level details</li>
</ol>
<p>The framework is smart about what it tracks. It doesn&#39;t just dump everything into a log file like some solutions. Instead, it:</p>
<ul>
<li><strong>Serializes complex objects</strong> properly (goodbye, <code>[object Object]</code> logs)</li>
<li><strong>Handles collections</strong> intelligently</li>
<li><strong>Only tracks actual changes</strong> (not just &quot;something was saved&quot;)</li>
<li><strong>Maintains referential integrity</strong> with primary key tracking</li>
</ul>
<h2 id="lessons-learned-building-this">Lessons Learned Building This</h2>
<h3 id="start-simple-stay-flexible">Start Simple, Stay Flexible</h3>
<p>We started with basic audit logging and gradually added sophistication. The framework can handle complex scenarios now (like tracking changes to collections of related entities), but it started life tracking simple property changes.</p>
<h3 id="performance-matters">Performance Matters</h3>
<p>Adding audit trails to every database operation could theoretically slow things down. In practice? We&#39;ve seen zero noticeable impact. Entity Framework&#39;s change tracking is already doing most of the heavy lifting, so we&#39;re just piggyback riding on that work.</p>
<h3 id="json-is-your-friend">JSON is Your Friend</h3>
<p>Storing field changes as JSON gives us incredible flexibility. We can query it, we can deserialize it for reports, and we don&#39;t have to predict every possible audit scenario upfront.</p>
<h2 id="what-and-39-s-next">What&#39;s Next</h2>
<p>The audit framework is just the foundation. We&#39;re already thinking about:</p>
<ul>
<li><strong>Real-time audit dashboards</strong> for monitoring platform health</li>
<li><strong>AI-powered anomaly detection</strong> to catch suspicious patterns</li>
<li><strong>Automated compliance reporting</strong> for when we inevitably need SOC 2 certification</li>
<li><strong>User-facing audit logs</strong> so people can see their own activity</li>
</ul>
<h2 id="the-bottom-line">The Bottom Line</h2>
<p>Building AspireHire isn&#39;t just about connecting job seekers with employers—it&#39;s about doing it right. That means thinking about data integrity, compliance, and trust from day one, not retrofitting it later when some crisis forces our hand.</p>
<p>The audit framework might not be the flashiest feature we&#39;ll ever build, but it&#39;s the kind of foundational work that separates professional platforms from hobby projects. When candidates trust us with their career data and employers trust us with their hiring processes, we better have our act together.</p>
<p>And honestly? There&#39;s something deeply satisfying about knowing that every single change in our system is tracked, auditable, and recoverable. It&#39;s like having a really good backup strategy—you hope you never need it, but you sleep better knowing it&#39;s there.</p>
<hr>
<p><em>Building AspireHire has been a masterclass in balancing feature velocity with technical excellence. Want to follow along with our journey? Check out our other posts about the platform architecture, or better yet, come try the platform when we launch. We&#39;re building something special here, one properly-audited database change at a time.</em></p>
<p>💡 Want to see how it all comes together in real code? The full implementation of the AspireHire audit framework complete with AuditDBContext, entity classes, and sample usage, is up on GitHub. Check it out, fork it, or even open an issue if you’ve got ideas for making it better.</p>
<p>👉 github.com/architect4hire/aspirehire</p>
]]></content:encoded>
        </item>
        <item>
            <title>Kicking Off AspireHire</title>
            <link>https://www.architect4hire.com/blog/kicking-off-aspirehire</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/kicking-off-aspirehire</guid>
            <pubDate>Fri, 29 Aug 2025 11:31:07 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Building AspireHire</category>
            <description>The world does not need another side project that fizzles out after a GitHub repo and a half-finished README. It needs projects that teach as they grow. This is the blueprint for AspireHire — and the series that builds it in the open.</description>
            <content:encoded><![CDATA[<h3 id="building-a-proof-of-concept-and-teaching-developers-how-to-master-modern-asp-net-core-aspire-and-azure-development">Building a Proof of Concept and Teaching Developers How to Master Modern ASP.NET Core, Aspire, and Azure Development</h3>
<h2 id="setting-the-stage">Setting the Stage</h2>
<p>Let’s be real for a second. The world does not need another side project that fizzles out after a GitHub repo and a half-finished README. What it does need is projects that actually push developers forward, projects that teach as they grow, and projects that solve a real problem.</p>
<p>That’s why I’m launching <strong>AspireHire</strong>. At the surface, AspireHire looks like another freelance platform. The twist is that this one is built for developers, by a developer, with the entire journey shared in public. AspireHire is a proof of concept that doubles as a teaching tool. Over the next series of posts, I am going to break down every architectural choice, every piece of code, and every pipeline step so you can learn exactly how to build modern, cloud-native applications with ASP.NET Core 9, .NET Aspire, and Azure.</p>
<p>Think of this as a mix between a product launch and a masterclass in distributed system design. By the end, you will not only understand what AspireHire is but also how to apply the same patterns in your own work.</p>
<hr>
<h2 id="why-build-another-freelance-platform">Why Build Another Freelance Platform?</h2>
<p>If you’ve ever used Upwork or Fiverr, you know how they work. Upwork focuses on long-term projects and proposals. Fiverr is all about fast gigs at fixed prices. Both serve their markets well, but they share one big flaw: they don’t let developers show off what makes them special.</p>
<p>On Upwork you can write a nice cover letter and maybe share a portfolio. On Fiverr you can list a gig with a fixed price. Neither really helps a client figure out who is a serious technical professional and who just knows how to fill out a profile.</p>
<p>AspireHire flips the script. Instead of treating developers like interchangeable parts, it highlights what makes each one unique. Profiles include real portfolios, skill tags, certifications, GitHub integrations, and even coding test results. Clients can finally see more than a name, a rating, and a price tag.</p>
<p>This niche focus matters. By building only for software and IT work, AspireHire avoids the trap of being too broad. Developers get tools designed for them, and clients get confidence that they are hiring skilled professionals.</p>
<p>And for me, this is not just a product idea. It is a chance to show developers how to build a system like this from the ground up.</p>
<hr>
<h2 id="from-idea-to-proof-of-concept">From Idea to Proof of Concept</h2>
<p>Here is the important thing to understand. AspireHire is not just about launching another app. This project is a <strong>playground for learning</strong>. It is where I get to show you how a professional architect thinks about requirements, system design, data modeling, microservices, observability, and DevOps pipelines.</p>
<p>Instead of just handing you a GitHub repo at the end, I am going to walk you through the entire build. You will see the mistakes, the fixes, and the tradeoffs. You will learn not just what works but why it works.</p>
<p>If you have ever wanted to level up from hobby projects and tutorials into building production-ready systems, this series is going to give you that push.</p>
<hr>
<h2 id="what-the-series-will-cover">What the Series Will Cover</h2>
<p>Let me give you a roadmap of what’s coming. Each post will dive into a piece of AspireHire, with real examples and code along the way.</p>
<ol>
<li><strong>From Requirements to Architecture</strong>
How to translate a vision into a technical blueprint. We will break down roles, workflows, and data models, then map them to services.</li>
<li><strong>Setting Up .NET Aspire</strong>
Why Aspire is a game-changer for cloud-native apps and how to use it for service discovery, observability, and local orchestration.</li>
<li><strong>Microservice by Microservice</strong>
Building Auth, Profile, Job, Proposal, Contract, Payment, and Messaging services with ASP.NET Core 9 and Clean Architecture.</li>
<li><strong>Relational Data Models with EF Core</strong>
Designing tables and relationships that support complex workflows like proposals, contracts, and reviews.</li>
<li><strong>Event-Driven Messaging</strong>
Using Azure Service Bus to decouple services and handle workflows like notifications and escrow payments.</li>
<li><strong>CI/CD Pipelines with Azure DevOps</strong>
Containerizing everything, pushing to Azure Container Registry, and deploying to AKS with zero downtime.</li>
<li><strong>Observability that Actually Works</strong>
Logs, metrics, and traces using .NET Aspire defaults, OpenTelemetry, and Application Insights.</li>
<li><strong>Securing the Platform</strong>
Authentication with Azure AD B2C, protecting secrets with Key Vault, and designing for PCI and GDPR compliance.</li>
<li><strong>Building a Developer-First UI</strong>
Creating a web experience that makes sense for technical users, from dark mode to GitHub integrations.</li>
<li><strong>Scaling and Extending</strong>
Planning for AI-powered recommendations, advanced search, and premium features once the core platform is solid.</li>
</ol>
<p>By the time we are done, you will have the skills and the code samples to build your own distributed system on Azure.</p>
<hr>
<h2 id="the-starting-blueprint">The Starting Blueprint</h2>
<p>Every project starts with a mental picture of what it will look like in production. Here’s the high-level blueprint for AspireHire:</p>
<ul>
<li><strong>Platform</strong>: ASP.NET Core 9 with .NET Aspire</li>
<li><strong>Deployment</strong>: Azure Kubernetes Service</li>
<li><strong>Storage</strong>: Azure SQL for relational data, Blob Storage for files, Redis for caching</li>
<li><strong>Messaging</strong>: Azure Service Bus for events</li>
<li><strong>Observability</strong>: Application Insights with OpenTelemetry integration</li>
<li><strong>CI/CD</strong>: Azure DevOps Pipelines pushing Docker images to Azure Container Registry</li>
</ul>
<p><strong>The core microservices include:</strong></p>
<ul>
<li><strong>Auth Service</strong> for login and identity</li>
<li><strong>Profile Service</strong> for detailed freelancer and client profiles</li>
<li><strong>Job Service</strong> for creating and searching job postings</li>
<li><strong>Proposal Service</strong> for bids and negotiations</li>
<li><strong>Contract and Payment Service</strong> for escrow and transactions</li>
<li><strong>Messaging Service</strong> for real-time communication tied to jobs</li>
</ul>
<p>Each service owns its own data store and can scale independently in AKS. All of them get logging, tracing, and health checks out of the box with Aspire.</p>
<hr>
<h2 id="why-use-net-aspire">Why Use .NET Aspire?</h2>
<p>If you have ever built microservices the old way, you know the pain. You spend hours wiring up logging, tracing, configuration, and health checks before you even start writing business logic. Local orchestration is a mess.</p>
<p>.NET Aspire fixes that. With one method call, <code>AddServiceDefaults</code>, you get logging, tracing, metrics, and health endpoints already wired. Service discovery and orchestration happen in the AppHost, so running your entire stack locally feels like magic.</p>
<p>When you combine Aspire with Azure’s managed services, you get speed and reliability. Developers spend less time on plumbing and more time building features. That is why AspireHire is the perfect project to showcase what Aspire brings to the table.</p>
<hr>
<h2 id="building-with-azure-in-mind">Building with Azure in Mind</h2>
<p>This project is built on Azure from the start. Not bolted on later. Not a side thought. From day one the services run on AKS and integrate with managed Azure services.</p>
<ul>
<li><strong>AKS</strong> gives us container orchestration, scaling, and zero-downtime updates</li>
<li><strong>Azure SQL Database</strong> handles relational storage with built-in HA and backups</li>
<li><strong>Azure Blob Storage</strong> stores profile images, portfolio files, and message attachments</li>
<li><strong>Azure Service Bus</strong> manages asynchronous communication between services</li>
<li><strong>Application Insights</strong> collects telemetry and connects to Aspire’s OpenTelemetry pipeline</li>
<li><strong>Key Vault</strong> protects secrets and connects seamlessly to services through managed identities</li>
</ul>
<p>By designing cloud-first, we get to learn not just how to build apps but how to build apps that run reliably in the environments companies actually use.</p>
<hr>
<h2 id="teaching-through-real-problems">Teaching Through Real Problems</h2>
<p>One promise I want to make is this: I am not going to hide the rough edges. You will see the broken deployments. You will see the EF Core migrations that fail. You will see the pipeline steps that don’t work the first time.</p>
<p>Because that is how real projects go. The value is not in seeing a perfectly polished example. The value is in seeing how to troubleshoot and fix problems like a professional. That is how you level up as a developer.</p>
<hr>
<h2 id="what-you-will-get-out-of-this">What You Will Get Out of This</h2>
<p>If you stick with this series, here is what you will walk away with:</p>
<ul>
<li>Confidence to design and implement distributed systems on Azure</li>
<li>Hands-on examples of microservice patterns in ASP.NET Core 9</li>
<li>Real pipelines you can adapt for your own projects</li>
<li>A clear mental model of how to debug, monitor, and scale apps in production</li>
<li>A stronger resume, GitHub portfolio, or skill set for your next role or contract</li>
</ul>
<p>This is not another “todo list in microservices.” This is the kind of project you can point to in an interview and say, “Yes, I know how to build that.”</p>
<hr>
<h2 id="where-we-go-next">Where We Go Next</h2>
<p>This post is the kickoff. In the next one, we are going to dive into the architecture and requirements document in detail. I will show you how to take an idea like “freelance platform for developers” and turn it into concrete services, data models, and diagrams.</p>
<p>From there we will start building the services piece by piece. By the end, AspireHire will not only exist as a working app but also as a step-by-step guide to modern .NET cloud development.</p>
<hr>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>AspireHire is both a product experiment and a learning journey. By focusing on developers as users, it solves a real gap in the freelance market. By building it out loud, it creates a chance for you to see exactly how modern cloud apps are designed and deployed.</p>
<p>If you have been waiting for a project that goes deeper than tutorials and takes you inside the head of an architect, this is it. The best way to learn is by building something real, and that is exactly what we are about to do together.</p>
<p>So grab your coffee, fire up your IDE, and get ready. This is going to be a fun ride.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Outbox Pattern and the Two-Stage Commit Problem</title>
            <link>https://www.architect4hire.com/blog/outbox-pattern-and-the-two-stage-commit-problem</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/outbox-pattern-and-the-two-stage-commit-problem</guid>
            <pubDate>Thu, 21 Aug 2025 10:31:26 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Architecture &amp; Patterns</category>
            <description>Save to the database, publish an event. Two writes, one of which can fail — and now your system is lying to itself. The Outbox Pattern makes the business operation and the event publish atomic, and this is how you actually implement it.</description>
            <content:encoded><![CDATA[<p>In the world of microservices and distributed systems, ensuring reliable message delivery while maintaining data consistency is one of the most challenging problems developers face. The <strong>Outbox Pattern</strong> provides an elegant solution to this dual-write problem, guaranteeing that business operations and event publishing happen atomically. This comprehensive guide will walk you through the implementation of the Outbox Pattern step-by-step, discuss the requirements for success, and highlight critical considerations.</p>
<h2 id="understanding-the-problem-the-dual-write-challenge">Understanding the Problem: The Dual-Write Challenge</h2>
<p>Before diving into the solution, let&#39;s understand the problem the Outbox Pattern solves. Consider this common scenario in a microservices architecture:</p>
<ol>
<li>A service processes a business operation (e.g., creating an order)</li>
<li>The service needs to persist data to a database</li>
<li>The service must publish an event to notify other services</li>
</ol>
<p>The challenge arises when trying to make both operations atomic. What happens if the database transaction succeeds but the message publishing fails? Or vice versa? This dual-write problem can lead to:</p>
<ul>
<li><strong>Data inconsistency</strong> between services</li>
<li><strong>Lost events</strong> that other services never receive</li>
<li><strong>Duplicate processing</strong> when retries occur</li>
<li><strong>System-wide reliability issues</strong></li>
</ul>
<h2 id="the-outbox-pattern-solution">The Outbox Pattern Solution</h2>
<p>The Outbox Pattern solves this by storing outbound messages in the same database transaction as the business data. A separate background process then reliably publishes these messages to the message broker. This approach ensures <strong>exactly-once delivery semantics</strong> and maintains consistency between local state changes and event publishing.</p>
<h3 id="system-components">System Components</h3>
<p>The solution consists of five main components:</p>
<ol>
<li><strong>OrderCoordinator</strong> - ASP.NET Core Web API (.NET 9)</li>
<li><strong>InventoryApi</strong> - ASP.NET Core Web API (.NET 9)</li>
<li><strong>PaymentsApi</strong> - ASP.NET Core Web API (.NET 9)</li>
<li><strong>RollbackWorker</strong> - Azure Functions (.NET 8)</li>
<li><strong>Contracts</strong> - Shared contract library (.NET 9)</li>
<li><strong>Azure Service Bus</strong> - Message broker for async communication</li>
</ol>
<h3 id="architectural-patterns">Architectural Patterns</h3>
<h4 id="1-microservices-architecture">1. <strong>Microservices Architecture</strong></h4>
<ul>
<li>Each service owns its data (SQLite databases)</li>
<li>Services communicate via HTTP APIs and async messaging</li>
<li>Loose coupling through well-defined contracts</li>
</ul>
<h4 id="2-outbox-pattern-implementation">2. <strong>Outbox Pattern Implementation</strong></h4>
<p>The <strong>Outbox Pattern</strong> is implemented in the <code>OrderCoordinator</code> service to ensure reliable message publishing:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">sealed</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OutboxMessage</span>
{
    <span class="hljs-keyword">public</span> Guid Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = Guid.NewGuid();
    <span class="hljs-keyword">public</span> DateTime OccurredUtc { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = DateTime.UtcNow;
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Type { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = <span class="hljs-literal">default</span>!;
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> PayloadJson { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = <span class="hljs-literal">default</span>!;
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> AttemptCount { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> DateTime? LastAttemptUtc { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> OutboxMessage <span class="hljs-title">From</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">T payload</span>) <span class="hljs-keyword">where</span> T : <span class="hljs-keyword">class</span></span>
        =&gt; <span class="hljs-keyword">new</span> OutboxMessage
        {
            Type = <span class="hljs-keyword">typeof</span>(T).FullName!,
            PayloadJson = JsonSerializer.Serialize(payload)
        };
}</code></pre>
<p><strong>Key Implementation Details:</strong></p>
<ol>
<li><strong>Transactional Consistency</strong>: Outbox messages are stored in the same database transaction as business operations</li>
<li><strong>Background Publisher</strong>: <code>OutboxPublisher</code> runs as a <code>BackgroundService</code> polling for pending messages</li>
<li><strong>Retry Logic</strong>: Failed messages track attempt counts and timestamps</li>
<li><strong>At-Least-Once Delivery</strong>: Messages are only removed after successful publishing</li>
</ol>
<h4 id="3-orchestration-based-saga-pattern">3. <strong>Orchestration-Based Saga Pattern</strong></h4>
<p>The <code>OrderCoordinator</code> implements an orchestration saga with compensating transactions:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Try-Confirm-Cancel pattern</span>
<span class="hljs-keyword">try</span>
{
    <span class="hljs-comment">// Try: Reserve inventory</span>
    <span class="hljs-keyword">var</span> resResp = <span class="hljs-keyword">await</span> inventory.PostAsJsonAsync(<span class="hljs-string">&quot;inventory/reservations&quot;</span>, ...);

    <span class="hljs-comment">// Try: Authorize payment</span>
    <span class="hljs-keyword">var</span> authResp = <span class="hljs-keyword">await</span> payments.PostAsJsonAsync(<span class="hljs-string">&quot;payments/authorize&quot;</span>, ...);

    <span class="hljs-comment">// Confirm both operations</span>
    <span class="hljs-keyword">await</span> inventory.PostAsJsonAsync(<span class="hljs-string">&quot;inventory/reservations/confirm&quot;</span>, ...);
    <span class="hljs-keyword">await</span> payments.PostAsJsonAsync(<span class="hljs-string">&quot;payments/capture&quot;</span>, ...);
}
<span class="hljs-keyword">catch</span> (Exception ex)
{
    <span class="hljs-comment">// Compensate on failure</span>
    db.Outbox.Add(OutboxMessage.From(<span class="hljs-keyword">new</span> OrderRollbackRequested(...)));
}</code></pre>
<h4 id="4-idempotency-pattern">4. <strong>Idempotency Pattern</strong></h4>
<p>Both the order coordinator and downstream services implement idempotency:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Order coordinator uses idempotency keys</span>
<span class="hljs-keyword">var</span> idem = <span class="hljs-keyword">await</span> db.Idempotency.FirstOrDefaultAsync(i =&gt; i.Key == idempotencyKey, ct);
<span class="hljs-keyword">if</span> (idem <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">return</span> Results.Accepted(<span class="hljs-string">$&quot;/orders/<span class="hljs-subst">{idem.ResultOrderId}</span>&quot;</span>, <span class="hljs-keyword">new</span> { orderId = idem.ResultOrderId });

<span class="hljs-comment">// Downstream services check for existing operations</span>
<span class="hljs-keyword">var</span> existing = <span class="hljs-keyword">await</span> db.Authorizations.FirstOrDefaultAsync(a =&gt; a.OrderId == req.OrderId);
<span class="hljs-keyword">if</span> (existing <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">return</span> Results.Ok(<span class="hljs-keyword">new</span> AuthorizePaymentResponse(existing.AuthorizationId));</code></pre>
<h2 id="step-by-step-implementation-guide">Step-by-Step Implementation Guide</h2>
<h3 id="step-1-design-the-outbox-message-entity">Step 1: Design the Outbox Message Entity</h3>
<p>The foundation of the Outbox Pattern is a database table that stores pending messages. Here&#39;s a well-designed outbox entity:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">sealed</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OutboxMessage</span>
{
    <span class="hljs-keyword">public</span> Guid Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = Guid.NewGuid();
    <span class="hljs-keyword">public</span> DateTime OccurredUtc { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = DateTime.UtcNow;
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Type { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = <span class="hljs-literal">default</span>!;
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> PayloadJson { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; } = <span class="hljs-literal">default</span>!;
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> AttemptCount { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> DateTime? LastAttemptUtc { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> OutboxMessage <span class="hljs-title">From</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">T payload</span>) <span class="hljs-keyword">where</span> T : <span class="hljs-keyword">class</span></span>
        =&gt; <span class="hljs-keyword">new</span> OutboxMessage
        {
            Type = <span class="hljs-keyword">typeof</span>(T).FullName!,
            PayloadJson = JsonSerializer.Serialize(payload)
        };
}</code></pre>
<p><strong>Key Design Decisions:</strong></p>
<ul>
<li><strong>Immutable core properties</strong>: <code>Id</code>, <code>OccurredUtc</code>, <code>Type</code>, and <code>PayloadJson</code> are set once and never changed</li>
<li><strong>Tracking metadata</strong>: <code>AttemptCount</code> and <code>LastAttemptUtc</code> support retry logic and monitoring</li>
<li><strong>Generic serialization</strong>: The <code>From</code> method provides type-safe message creation</li>
<li><strong>Type information</strong>: Storing the full type name enables proper deserialization</li>
</ul>
<h3 id="step-2-integrate-with-your-data-model">Step 2: Integrate with Your Data Model</h3>
<p>Add the outbox table to your existing <code>DbContext</code>:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">sealed</span> <span class="hljs-keyword">class</span> <span class="hljs-title">AppDb</span>(<span class="hljs-params">DbContextOptions&lt;AppDb&gt; opt</span>) : <span class="hljs-title">DbContext</span>(<span class="hljs-params">opt</span>)</span>
{
    <span class="hljs-keyword">public</span> DbSet&lt;OrderRecord&gt; Orders =&gt; Set&lt;OrderRecord&gt;();
    <span class="hljs-keyword">public</span> DbSet&lt;IdempotencyRecord&gt; Idempotency =&gt; Set&lt;IdempotencyRecord&gt;();
    <span class="hljs-keyword">public</span> DbSet&lt;OutboxMessage&gt; Outbox =&gt; Set&lt;OutboxMessage&gt;();

    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnModelCreating</span>(<span class="hljs-params">ModelBuilder modelBuilder</span>)</span>
    {
        <span class="hljs-comment">// Add indexes for efficient querying</span>
        modelBuilder.Entity&lt;OutboxMessage&gt;()
            .HasIndex(x =&gt; x.OccurredUtc)
            .HasDatabaseName(<span class="hljs-string">&quot;IX_OutboxMessage_OccurredUtc&quot;</span>);

        modelBuilder.Entity&lt;OutboxMessage&gt;()
            .HasIndex(x =&gt; <span class="hljs-keyword">new</span> { x.AttemptCount, x.LastAttemptUtc })
            .HasDatabaseName(<span class="hljs-string">&quot;IX_OutboxMessage_Retry&quot;</span>);
    }
}</code></pre>
<h3 id="step-3-implement-transactional-message-storage">Step 3: Implement Transactional Message Storage</h3>
<p>The critical aspect is storing outbox messages within the same database transaction as your business operations:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IResult&gt; <span class="hljs-title">PlaceOrderAsync</span>(<span class="hljs-params">CreateOrderDto dto, <span class="hljs-built_in">string</span> idempotencyKey, CancellationToken ct</span>)</span>
{
    <span class="hljs-comment">// Check idempotency first</span>
    <span class="hljs-keyword">var</span> idem = <span class="hljs-keyword">await</span> db.Idempotency.FirstOrDefaultAsync(i =&gt; i.Key == idempotencyKey, ct);
    <span class="hljs-keyword">if</span> (idem <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>)
        <span class="hljs-keyword">return</span> Results.Accepted(<span class="hljs-string">$&quot;/orders/<span class="hljs-subst">{idem.ResultOrderId}</span>&quot;</span>, <span class="hljs-keyword">new</span> { orderId = idem.ResultOrderId });

    <span class="hljs-keyword">var</span> order = <span class="hljs-keyword">new</span> OrderRecord { Id = Guid.NewGuid(), Amount = dto.Amount, Status = <span class="hljs-string">&quot;Pending&quot;</span> };
    db.Orders.Add(order);
    <span class="hljs-keyword">await</span> db.SaveChangesAsync(ct);

    <span class="hljs-keyword">try</span>
    {
        <span class="hljs-comment">// Business logic with external services</span>
        <span class="hljs-keyword">await</span> ProcessInventoryReservation(order, dto.Items, ct);
        <span class="hljs-keyword">await</span> ProcessPaymentAuthorization(order, ct);
        <span class="hljs-keyword">await</span> ConfirmOrderComponents(order, ct);

        order.Status = <span class="hljs-string">&quot;Confirmed&quot;</span>;
    }
    <span class="hljs-keyword">catch</span> (Exception ex)
    {
        order.Status = <span class="hljs-string">&quot;Canceled&quot;</span>;
        <span class="hljs-comment">// Store rollback event in the same transaction</span>
        db.Outbox.Add(OutboxMessage.From(<span class="hljs-keyword">new</span> OrderRollbackRequested(
            order.Id, 
            order.InventoryReservationId, 
            order.PaymentAuthId, 
            ex.Message)));
    }

    <span class="hljs-comment">// Store idempotency record and save everything atomically</span>
    db.Idempotency.Add(<span class="hljs-keyword">new</span> IdempotencyRecord { Key = idempotencyKey, ResultOrderId = order.Id });
    <span class="hljs-keyword">await</span> db.SaveChangesAsync(ct);

    <span class="hljs-keyword">return</span> Results.Accepted(<span class="hljs-string">$&quot;/orders/<span class="hljs-subst">{order.Id}</span>&quot;</span>, <span class="hljs-keyword">new</span> { orderId = order.Id });
}</code></pre>
<p><strong>Critical Points:</strong></p>
<ul>
<li>Outbox messages are added to the same <code>DbContext</code> as business entities</li>
<li>A single <code>SaveChangesAsync()</code> call ensures atomicity</li>
<li>If the transaction fails, neither business data nor outbox messages are persisted</li>
</ul>
<h3 id="step-4-build-the-background-publisher">Step 4: Build the Background Publisher</h3>
<p>The background service continuously polls for pending messages and publishes them:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OutboxPublisher</span>(<span class="hljs-params">IServiceProvider sp, IConfiguration cfg, ILogger&lt;OutboxPublisher&gt; log</span>) : BackgroundService</span>
{
    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">ExecuteAsync</span>(<span class="hljs-params">CancellationToken stoppingToken</span>)</span>
    {
        <span class="hljs-keyword">var</span> pollSeconds = cfg.GetValue&lt;<span class="hljs-built_in">int</span>?&gt;(<span class="hljs-string">&quot;Outbox:PollSeconds&quot;</span>) ?? <span class="hljs-number">1</span>;
        <span class="hljs-keyword">var</span> batchSize = cfg.GetValue&lt;<span class="hljs-built_in">int</span>?&gt;(<span class="hljs-string">&quot;Outbox:BatchSize&quot;</span>) ?? <span class="hljs-number">100</span>;
        <span class="hljs-keyword">var</span> topicName = cfg[<span class="hljs-string">&quot;ServiceBus:TopicName&quot;</span>] ?? <span class="hljs-string">&quot;orders&quot;</span>;

        <span class="hljs-keyword">while</span> (!stoppingToken.IsCancellationRequested)
        {
            <span class="hljs-keyword">try</span>
            {
                <span class="hljs-keyword">await</span> ProcessPendingMessages(batchSize, topicName, stoppingToken);
            }
            <span class="hljs-keyword">catch</span> (Exception ex)
            {
                log.LogError(ex, <span class="hljs-string">&quot;Outbox processing loop error&quot;</span>);
            }

            <span class="hljs-keyword">await</span> Task.Delay(TimeSpan.FromSeconds(pollSeconds), stoppingToken);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">ProcessPendingMessages</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> batchSize, <span class="hljs-built_in">string</span> topicName, CancellationToken stoppingToken</span>)</span>
    {
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> scope = sp.CreateScope();
        <span class="hljs-keyword">var</span> db = scope.ServiceProvider.GetRequiredService&lt;AppDb&gt;();
        <span class="hljs-keyword">var</span> sbClient = scope.ServiceProvider.GetRequiredService&lt;ServiceBusClient&gt;();
        <span class="hljs-keyword">var</span> sender = sbClient.CreateSender(topicName);

        <span class="hljs-keyword">var</span> pending = <span class="hljs-keyword">await</span> db.Outbox
            .OrderBy(x =&gt; x.OccurredUtc)
            .Take(batchSize)
            .ToListAsync(stoppingToken);

        <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> message <span class="hljs-keyword">in</span> pending)
        {
            <span class="hljs-keyword">try</span>
            {
                <span class="hljs-keyword">await</span> PublishMessage(sender, message, stoppingToken);
                db.Outbox.Remove(message);
            }
            <span class="hljs-keyword">catch</span> (Exception ex)
            {
                <span class="hljs-keyword">await</span> HandlePublishFailure(message, ex);
            }
        }

        <span class="hljs-keyword">await</span> db.SaveChangesAsync(stoppingToken);
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">PublishMessage</span>(<span class="hljs-params">ServiceBusSender sender, OutboxMessage outboxMessage, CancellationToken ct</span>)</span>
    {
        <span class="hljs-keyword">var</span> message = <span class="hljs-keyword">new</span> ServiceBusMessage(outboxMessage.PayloadJson)
        {
            Subject = outboxMessage.Type,
            MessageId = outboxMessage.Id.ToString(),
            ContentType = <span class="hljs-string">&quot;application/json&quot;</span>
        };

        <span class="hljs-keyword">await</span> sender.SendMessageAsync(message, ct);
        log.LogInformation(<span class="hljs-string">&quot;Published message {MessageId} of type {Type}&quot;</span>, 
            outboxMessage.Id, outboxMessage.Type);
    }

    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">HandlePublishFailure</span>(<span class="hljs-params">OutboxMessage message, Exception ex</span>)</span>
    {
        message.AttemptCount++;
        message.LastAttemptUtc = DateTime.UtcNow;

        log.LogWarning(ex, <span class="hljs-string">&quot;Failed to publish message {MessageId} (attempt {AttemptCount})&quot;</span>, 
            message.Id, message.AttemptCount);

        <span class="hljs-comment">// Implement exponential backoff or dead letter logic here</span>
        <span class="hljs-keyword">if</span> (message.AttemptCount &gt;= <span class="hljs-number">5</span>)
        {
            log.LogError(<span class="hljs-string">&quot;Message {MessageId} exceeded max retry attempts&quot;</span>, message.Id);
            <span class="hljs-comment">// Consider moving to dead letter storage</span>
        }
    }
}</code></pre>
<h3 id="step-5-configure-dependency-injection">Step 5: Configure Dependency Injection</h3>
<p>Register all components in your DI container:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">var</span> builder = WebApplication.CreateBuilder(<span class="hljs-keyword">args</span>);

<span class="hljs-comment">// Database</span>
builder.Services.AddDbContext&lt;AppDb&gt;(opt =&gt; 
    opt.UseSqlite(builder.Configuration.GetConnectionString(<span class="hljs-string">&quot;AppDb&quot;</span>)));

<span class="hljs-comment">// Message Broker</span>
builder.Services.AddSingleton(sp =&gt;
{
    <span class="hljs-keyword">var</span> cfg = sp.GetRequiredService&lt;IConfiguration&gt;();
    <span class="hljs-keyword">var</span> conn = cfg[<span class="hljs-string">&quot;ServiceBus:ConnectionString&quot;</span>]!;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ServiceBusClient(conn);
});

<span class="hljs-comment">// Background Service</span>
builder.Services.AddHostedService&lt;OutboxPublisher&gt;();

<span class="hljs-comment">// HTTP Clients for external services</span>
builder.Services.AddHttpClient(<span class="hljs-string">&quot;Inventory&quot;</span>, c =&gt;
{
    c.BaseAddress = <span class="hljs-keyword">new</span> Uri(builder.Configuration[<span class="hljs-string">&quot;Services:Inventory&quot;</span>]!);
});
builder.Services.AddHttpClient(<span class="hljs-string">&quot;Payments&quot;</span>, c =&gt;
{
    c.BaseAddress = <span class="hljs-keyword">new</span> Uri(builder.Configuration[<span class="hljs-string">&quot;Services:Payments&quot;</span>]!);
});

<span class="hljs-keyword">var</span> app = builder.Build();

<span class="hljs-comment">// Ensure database is created</span>
<span class="hljs-keyword">await</span> <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> scope = app.Services.CreateAsyncScope())
{
    <span class="hljs-keyword">var</span> db = scope.ServiceProvider.GetRequiredService&lt;AppDb&gt;();
    <span class="hljs-keyword">await</span> db.Database.EnsureCreatedAsync();
}</code></pre>
<h3 id="step-6-add-monitoring-and-observability">Step 6: Add Monitoring and Observability</h3>
<p>Create an endpoint to monitor outbox health:</p>
<pre class="blog-code"><code class="hljs language-csharp">app.MapGet(<span class="hljs-string">&quot;/outbox&quot;</span>, <span class="hljs-keyword">async</span> (AppDb db) =&gt;
{
    <span class="hljs-keyword">var</span> messages = <span class="hljs-keyword">await</span> db.Outbox
        .AsNoTracking()
        .OrderBy(x =&gt; x.OccurredUtc)
        .ToListAsync();

    <span class="hljs-keyword">return</span> Results.Ok(messages.Select(m =&gt; <span class="hljs-keyword">new</span> 
    { 
        m.Id, 
        m.Type, 
        m.AttemptCount, 
        m.LastAttemptUtc, 
        m.OccurredUtc,
        Status = m.AttemptCount == <span class="hljs-number">0</span> ? <span class="hljs-string">&quot;Pending&quot;</span> : 
                m.AttemptCount &lt; <span class="hljs-number">5</span> ? <span class="hljs-string">&quot;Retrying&quot;</span> : <span class="hljs-string">&quot;Failed&quot;</span>
    }));
});

app.MapGet(<span class="hljs-string">&quot;/outbox/health&quot;</span>, <span class="hljs-keyword">async</span> (AppDb db) =&gt;
{
    <span class="hljs-keyword">var</span> stats = <span class="hljs-keyword">await</span> db.Outbox
        .GroupBy(m =&gt; m.AttemptCount == <span class="hljs-number">0</span> ? <span class="hljs-string">&quot;Pending&quot;</span> : 
                     m.AttemptCount &lt; <span class="hljs-number">5</span> ? <span class="hljs-string">&quot;Retrying&quot;</span> : <span class="hljs-string">&quot;Failed&quot;</span>)
        .Select(g =&gt; <span class="hljs-keyword">new</span> { Status = g.Key, Count = g.Count() })
        .ToListAsync();

    <span class="hljs-keyword">var</span> oldestPending = <span class="hljs-keyword">await</span> db.Outbox
        .Where(m =&gt; m.AttemptCount == <span class="hljs-number">0</span>)
        .OrderBy(m =&gt; m.OccurredUtc)
        .Select(m =&gt; m.OccurredUtc)
        .FirstOrDefaultAsync();

    <span class="hljs-keyword">return</span> Results.Ok(<span class="hljs-keyword">new</span> 
    { 
        Statistics = stats,
        OldestPendingMessage = oldestPending,
        HealthStatus = oldestPending == <span class="hljs-literal">default</span> || 
                      DateTime.UtcNow - oldestPending &lt; TimeSpan.FromMinutes(<span class="hljs-number">5</span>) 
                      ? <span class="hljs-string">&quot;Healthy&quot;</span> : <span class="hljs-string">&quot;Warning&quot;</span>
    });
});</code></pre>
<h3 id="requirements-for-success">Requirements for Success</h3>
<h3 id="1-transactional-database-support">1. Transactional Database Support</h3>
<p>The Outbox Pattern <strong>requires</strong> a database that supports ACID transactions. This includes:</p>
<ul>
<li><strong>Relational databases</strong>: PostgreSQL, SQL Server, MySQL, SQLite</li>
<li><strong>Document databases with transactions</strong>: MongoDB (with replica sets), Azure Cosmos DB</li>
</ul>
<p><strong>Note</strong>: Simple key-value stores or eventually consistent databases won&#39;t work for this pattern.</p>
<h3 id="2-exactly-once-processing-logic">2. Exactly-Once Processing Logic</h3>
<p>Your business logic must be <strong>idempotent</strong> to handle potential duplicate processing:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Always check for existing records</span>
<span class="hljs-keyword">var</span> existing = <span class="hljs-keyword">await</span> db.Orders.FirstOrDefaultAsync(o =&gt; o.Id == orderId);
<span class="hljs-keyword">if</span> (existing != <span class="hljs-literal">null</span>)
{
    <span class="hljs-keyword">return</span> existing; <span class="hljs-comment">// Idempotent response</span>
}

<span class="hljs-comment">// Proceed with creation only if not exists</span>
<span class="hljs-keyword">var</span> order = <span class="hljs-keyword">new</span> OrderRecord { Id = orderId, <span class="hljs-comment">/* ... */</span> };</code></pre>
<h3 id="3-message-ordering-considerations">3. Message Ordering Considerations</h3>
<p>If message order matters, consider these strategies:</p>
<ul>
<li><strong>Partition by entity</strong>: Group related messages using partition keys</li>
<li><strong>Sequence numbers</strong>: Add ordering metadata to messages</li>
<li><strong>Single-threaded processing</strong>: Process messages sequentially (impacts throughput)</li>
</ul>
<h3 id="4-error-handling-and-dead-letter-management">4. Error Handling and Dead Letter Management</h3>
<p>Implement robust retry logic with exponential backoff:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> TimeSpan[] RetryDelays = 
{
    TimeSpan.FromSeconds(<span class="hljs-number">1</span>),
    TimeSpan.FromSeconds(<span class="hljs-number">5</span>),
    TimeSpan.FromSeconds(<span class="hljs-number">30</span>),
    TimeSpan.FromMinutes(<span class="hljs-number">5</span>),
    TimeSpan.FromMinutes(<span class="hljs-number">30</span>)
};

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ShouldRetry</span>(<span class="hljs-params">OutboxMessage message</span>)</span>
{
    <span class="hljs-keyword">if</span> (message.AttemptCount &gt;= RetryDelays.Length)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;

    <span class="hljs-keyword">var</span> nextRetryTime = message.LastAttemptUtc?.Add(RetryDelays[message.AttemptCount - <span class="hljs-number">1</span>]);
    <span class="hljs-keyword">return</span> nextRetryTime == <span class="hljs-literal">null</span> || DateTime.UtcNow &gt;= nextRetryTime;
}</code></pre>
<h3 id="5-performance-and-scalability">5. Performance and Scalability</h3>
<p>For high-throughput scenarios:</p>
<ul>
<li><strong>Batch processing</strong>: Process multiple messages in each iteration</li>
<li><strong>Parallel processing</strong>: Use multiple worker threads with proper coordination</li>
<li><strong>Database indexes</strong>: Ensure efficient querying of pending messages</li>
<li><strong>Message archival</strong>: Remove or archive successfully processed messages</li>
</ul>
<h2 id="words-of-caution">Words of Caution</h2>
<h3 id="1-storage-growth-management">1. Storage Growth Management</h3>
<p><strong>Critical Warning</strong>: The outbox table can grow rapidly in high-volume systems. Without proper cleanup, it can:</p>
<ul>
<li>Consume excessive storage space</li>
<li>Degrade query performance</li>
<li>Impact application startup times</li>
</ul>
<p><strong>Solution</strong>: Implement message cleanup after successful publishing:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Remove successful messages immediately</span>
db.Outbox.Remove(message);

<span class="hljs-comment">// Or archive for audit purposes</span>
message.ProcessedAt = DateTime.UtcNow;
message.Status = <span class="hljs-string">&quot;Completed&quot;</span>;</code></pre>
<h3 id="2-message-broker-dependency">2. Message Broker Dependency</h3>
<p>The background publisher creates a dependency on your message broker. Consider:</p>
<ul>
<li><strong>Circuit breaker patterns</strong> for broker outages</li>
<li><strong>Graceful degradation</strong> when messaging is unavailable</li>
<li><strong>Health checks</strong> to monitor broker connectivity</li>
</ul>
<h3 id="3-delayed-message-processing">3. Delayed Message Processing</h3>
<p>Messages aren&#39;t published immediately; there&#39;s a delay based on your polling interval. This means:</p>
<ul>
<li><strong>Near real-time requirements</strong> may not be suitable for this pattern</li>
<li><strong>Dependent services</strong> must handle eventual consistency</li>
<li><strong>User expectations</strong> should account for processing delays</li>
</ul>
<h3 id="4-database-transaction-size">4. Database Transaction Size</h3>
<p>Including outbox messages in business transactions can:</p>
<ul>
<li><strong>Increase transaction size</strong> and duration</li>
<li><strong>Impact database performance</strong> under high load</li>
<li><strong>Create contention</strong> on the outbox table</li>
</ul>
<p><strong>Mitigation</strong>: Consider using separate transactions for outbox writes in extreme high-volume scenarios, accepting the risk of lost messages for performance gains.</p>
<h3 id="5-message-duplication-scenarios">5. Message Duplication Scenarios</h3>
<p>Despite best efforts, duplicate messages can occur due to:</p>
<ul>
<li><strong>Publisher retries</strong> after timeout (message was actually sent)</li>
<li><strong>Database connection issues</strong> during acknowledgment</li>
<li><strong>Process crashes</strong> between publishing and cleanup</li>
</ul>
<p><strong>Solution</strong>: Always implement idempotent message handlers:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">HandleOrderCreated</span>(<span class="hljs-params">OrderCreated orderEvent</span>)</span>
{
    <span class="hljs-comment">// Check if already processed</span>
    <span class="hljs-keyword">var</span> existing = <span class="hljs-keyword">await</span> db.ProcessedEvents
        .FirstOrDefaultAsync(e =&gt; e.EventId == orderEvent.Id);

    <span class="hljs-keyword">if</span> (existing != <span class="hljs-literal">null</span>)
    {
        log.LogInformation(<span class="hljs-string">&quot;Event {EventId} already processed&quot;</span>, orderEvent.Id);
        <span class="hljs-keyword">return</span>; <span class="hljs-comment">// Idempotent exit</span>
    }

    <span class="hljs-comment">// Process the event</span>
    <span class="hljs-keyword">await</span> ProcessOrder(orderEvent);

    <span class="hljs-comment">// Record processing</span>
    db.ProcessedEvents.Add(<span class="hljs-keyword">new</span> ProcessedEvent 
    { 
        EventId = orderEvent.Id, 
        ProcessedAt = DateTime.UtcNow 
    });

    <span class="hljs-keyword">await</span> db.SaveChangesAsync();
}</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>The Outbox Pattern is a powerful solution for ensuring reliable message delivery in distributed systems. When implemented correctly, it provides:</p>
<ul>
<li><strong>Guaranteed message delivery</strong> with exactly-once semantics</li>
<li><strong>Data consistency</strong> between local and distributed state</li>
<li><strong>Resilience</strong> against partial failures</li>
<li><strong>Auditability</strong> of all system events</li>
</ul>
<p>However, success requires careful attention to transactional boundaries, idempotency, error handling, and performance considerations. The pattern is particularly valuable in mission-critical systems where data consistency and reliability are paramount.</p>
<p>Remember that the Outbox Pattern is one tool in the distributed systems toolkit. Evaluate whether its benefits outweigh the added complexity for your specific use case. For simple scenarios with relaxed consistency requirements, direct message publishing might suffice. For complex, business-critical systems, the Outbox Pattern provides the reliability guarantees that enable robust distributed architectures.</p>
<p>By following this guide and heeding the cautions outlined, you&#39;ll be well-equipped to implement a production-ready Outbox Pattern that serves as a reliable foundation for your microservices communication strategy.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Lessons Learned: Building Cloud-Native Microservices with ASP.NET Core 9 for Our Enterprise Client</title>
            <link>https://www.architect4hire.com/blog/lessons-learned-building-cloud-native-microservices-with-asp-net-core-9-for-our-enterprise-client</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/lessons-learned-building-cloud-native-microservices-with-asp-net-core-9-for-our-enterprise-client</guid>
            <pubDate>Mon, 21 Jul 2025 12:40:44 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Architecture &amp; Patterns</category>
            <description>&quot;Our system is slow, hard to scale, and every deployment is a nail-biting experience.&quot; Six months of modernizing a retail client&apos;s monolithic e-commerce platform — and the lessons that only show up in production.</description>
            <content:encoded><![CDATA[<p><em>A Real-World Journey from Requirements to Production</em></p>
<p>Hey fellow developers and architects! 👋</p>
<p>I wanted to share some insights from a recent project we completed for a major retail client who needed to modernize their monolithic e-commerce platform. They came to us with a classic story: &quot;Our system is slow, hard to scale, and every deployment is a nail-biting experience.&quot; Sound familiar?</p>
<p>After six months of intense development, testing, and deployment, we successfully delivered a cloud-native microservices architecture using ASP.NET Core 9, .NET Aspire, and Kubernetes. Here are the key lessons we learned along the way – the good, the challenging, and the &quot;wish we&#39;d known this earlier&quot; moments.</p>
<h2 id="the-client-challenge-why-we-chose-asp-net-core-9">The Client Challenge: Why We Chose ASP.NET Core 9</h2>
<p>Our client, a mid-size retail company, was struggling with their legacy .NET Framework application. Peak traffic during sales events would bring their system to its knees, and adding new features required coordinating deployments across multiple teams. They needed something modern, scalable, and maintainable.</p>
<p>After evaluating several options, we landed on ASP.NET Core 9 for several compelling reasons:</p>
<ul>
<li><strong>Performance</strong>: The client&#39;s existing .NET expertise meant easier team adoption</li>
<li><strong>Container-First Approach</strong>: Perfect fit for their cloud-native aspirations</li>
<li><strong>Microsoft Ecosystem</strong>: They were already invested in Azure</li>
<li><strong>Long-term Support</strong>: .NET 9&#39;s roadmap aligned with their strategic plans</li>
</ul>
<p>But here&#39;s the first lesson: <strong>choose your stack based on team capabilities, not just technical superiority</strong>. We could have gone with Go or Node.js, but the learning curve would have extended our timeline by months.</p>
<h2 id="lesson-1-net-aspire-is-a-game-changer-but-mind-the-learning-curve">Lesson #1: .NET Aspire is a Game-Changer (But Mind the Learning Curve)</h2>
<p>When we first heard about .NET Aspire, I&#39;ll be honest – I was skeptical. &quot;Another framework to learn?&quot; But after our initial spike, it became clear this wasn&#39;t just hype.</p>
<h3 id="what-aspire-got-right">What Aspire Got Right</h3>
<p>Here&#39;s how we structured our Aspire host for the client&#39;s catalog and inventory services:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">var</span> builder = DistributedApplication.CreateBuilder(<span class="hljs-keyword">args</span>);

<span class="hljs-comment">// Infrastructure components</span>
<span class="hljs-keyword">var</span> postgres = builder.AddPostgres(<span class="hljs-string">&quot;postgres&quot;</span>, port: <span class="hljs-number">5432</span>)
    .WithDatabase(<span class="hljs-string">&quot;catalogdb&quot;</span>)
    .WithDatabase(<span class="hljs-string">&quot;inventorydb&quot;</span>);

<span class="hljs-keyword">var</span> redis = builder.AddRedis(<span class="hljs-string">&quot;cache&quot;</span>);

<span class="hljs-keyword">var</span> serviceBus = builder.AddAzureServiceBus(<span class="hljs-string">&quot;messaging&quot;</span>);

<span class="hljs-comment">// Our microservices</span>
<span class="hljs-keyword">var</span> catalogApi = builder.AddProject&lt;Projects.CatalogApi&gt;(<span class="hljs-string">&quot;catalog-api&quot;</span>)
    .WithReference(postgres.GetDatabase(<span class="hljs-string">&quot;catalogdb&quot;</span>))
    .WithReference(redis)
    .WithReference(serviceBus);

<span class="hljs-keyword">var</span> inventoryApi = builder.AddProject&lt;Projects.InventoryApi&gt;(<span class="hljs-string">&quot;inventory-api&quot;</span>)
    .WithReference(postgres.GetDatabase(<span class="hljs-string">&quot;inventorydb&quot;</span>))
    .WithReference(redis)
    .WithReference(serviceBus);

<span class="hljs-keyword">var</span> webApp = builder.AddProject&lt;Projects.WebApp&gt;(<span class="hljs-string">&quot;webapp&quot;</span>)
    .WithReference(catalogApi)
    .WithReference(inventoryApi)
    .WithExternalHttpEndpoints();

builder.Build().Run();</code></pre>
<p><strong>The Magic</strong>: With just this configuration, we got:</p>
<ul>
<li>Automatic service discovery</li>
<li>Centralized configuration management</li>
<li>Built-in observability with OpenTelemetry</li>
<li>A gorgeous dashboard for local development</li>
</ul>
<p><strong>The Reality Check</strong>: Our junior developers initially struggled with the &quot;magic.&quot; When things worked, they worked beautifully. When they didn&#39;t, debugging required understanding several abstraction layers.</p>
<p><strong>Lesson Learned</strong>: Invest time upfront training your team on Aspire&#39;s internals. The productivity gains are massive once everyone understands what&#39;s happening under the hood.</p>
<h2 id="lesson-2-container-optimization-matters-more-than-you-think">Lesson #2: Container Optimization Matters More Than You Think</h2>
<p>One of our early mistakes was treating containerization as an afterthought. &quot;We&#39;ll just add a Dockerfile at the end,&quot; we thought. Wrong approach.</p>
<h3 id="the-problem-we-hit">The Problem We Hit</h3>
<p>Our initial container images were over 1GB, and startup times were exceeding 30 seconds in production. For a retail client expecting sub-second response times, this was unacceptable.</p>
<h3 id="the-solution-multi-stage-builds-and-alpine-images">The Solution: Multi-Stage Builds and Alpine Images</h3>
<p>Here&#39;s the optimized Dockerfile we ended up with:</p>
<pre class="blog-code"><code class="hljs language-dockerfile"><span class="hljs-comment"># Build stage</span>
<span class="hljs-keyword">FROM</span> mcr.microsoft.com/dotnet/sdk:<span class="hljs-number">9.0</span>-alpine AS build
<span class="hljs-keyword">WORKDIR</span><span class="language-bash"> /src</span>

<span class="hljs-comment"># Copy project files and restore dependencies</span>
<span class="hljs-keyword">COPY</span><span class="language-bash"> [<span class="hljs-string">&quot;CatalogApi/CatalogApi.csproj&quot;</span>, <span class="hljs-string">&quot;CatalogApi/&quot;</span>]</span>
<span class="hljs-keyword">COPY</span><span class="language-bash"> [<span class="hljs-string">&quot;CatalogApi.ServiceDefaults/CatalogApi.ServiceDefaults.csproj&quot;</span>, <span class="hljs-string">&quot;CatalogApi.ServiceDefaults/&quot;</span>]</span>
<span class="hljs-keyword">RUN</span><span class="language-bash"> dotnet restore <span class="hljs-string">&quot;CatalogApi/CatalogApi.csproj&quot;</span></span>

<span class="hljs-comment"># Copy source and build</span>
<span class="hljs-keyword">COPY</span><span class="language-bash"> . .</span>
<span class="hljs-keyword">WORKDIR</span><span class="language-bash"> <span class="hljs-string">&quot;/src/CatalogApi&quot;</span></span>
<span class="hljs-keyword">RUN</span><span class="language-bash"> dotnet build <span class="hljs-string">&quot;CatalogApi.csproj&quot;</span> -c Release -o /app/build</span>

<span class="hljs-comment"># Publish stage</span>
<span class="hljs-keyword">FROM</span> build AS publish
<span class="hljs-keyword">RUN</span><span class="language-bash"> dotnet publish <span class="hljs-string">&quot;CatalogApi.csproj&quot;</span> -c Release -o /app/publish /p:UseAppHost=<span class="hljs-literal">false</span></span>

<span class="hljs-comment"># Runtime stage</span>
<span class="hljs-keyword">FROM</span> mcr.microsoft.com/dotnet/aspnet:<span class="hljs-number">9.0</span>-alpine AS final
<span class="hljs-keyword">RUN</span><span class="language-bash"> apk add --no-cache icu-libs</span>
<span class="hljs-keyword">ENV</span> DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
<span class="hljs-keyword">WORKDIR</span><span class="language-bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="language-bash"> --from=publish /app/publish .</span>

<span class="hljs-comment"># Create non-root user</span>
<span class="hljs-keyword">RUN</span><span class="language-bash"> adduser -D -s /bin/sh appuser</span>
<span class="hljs-keyword">USER</span> appuser

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">8080</span>
<span class="hljs-keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="hljs-string">&quot;dotnet&quot;</span>, <span class="hljs-string">&quot;CatalogApi.dll&quot;</span>]</span></code></pre>
<p><strong>Results</strong>: Image size dropped to 180MB, startup time reduced to under 5 seconds.</p>
<p><strong>Lesson Learned</strong>: Container optimization isn&#39;t just about size – it&#39;s about security, startup time, and resource utilization. Start optimizing from day one, not as an afterthought.</p>
<h2 id="lesson-3-service-communication-strategy-is-critical">Lesson #3: Service Communication Strategy is Critical</h2>
<p>Initially, we went HTTP-everywhere. Every service talked to every other service via HTTP APIs. This seemed clean and RESTful, but we quickly ran into issues during peak loads.</p>
<h3 id="the-http-only-approach-our-first-attempt">The HTTP-Only Approach (Our First Attempt)</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// This seemed fine initially...</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> HttpClient _inventoryClient;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> HttpClient _catalogClient;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> HttpClient _paymentClient;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;OrderResult&gt; <span class="hljs-title">CreateOrderAsync</span>(<span class="hljs-params">CreateOrderRequest request</span>)</span>
    {
        <span class="hljs-comment">// Check inventory</span>
        <span class="hljs-keyword">var</span> inventory = <span class="hljs-keyword">await</span> _inventoryClient.GetAsync(<span class="hljs-string">$&quot;api/inventory/<span class="hljs-subst">{request.ProductId}</span>&quot;</span>);
        
        <span class="hljs-comment">// Get product details</span>
        <span class="hljs-keyword">var</span> product = <span class="hljs-keyword">await</span> _catalogClient.GetAsync(<span class="hljs-string">$&quot;api/products/<span class="hljs-subst">{request.ProductId}</span>&quot;</span>);
        
        <span class="hljs-comment">// Process payment</span>
        <span class="hljs-keyword">var</span> payment = <span class="hljs-keyword">await</span> _paymentClient.PostAsync(<span class="hljs-string">&quot;api/payments&quot;</span>, paymentData);
        
        <span class="hljs-comment">// Create order...</span>
    }
}</code></pre>
<p><strong>The Problem</strong>: During Black Friday testing, we discovered cascade failures. One slow service would cause timeouts across the entire chain.</p>
<h3 id="the-hybrid-approach-our-solution">The Hybrid Approach (Our Solution)</h3>
<p>We implemented a hybrid communication strategy:</p>
<p><strong>Synchronous (HTTP) for</strong>:</p>
<ul>
<li>User-facing operations requiring immediate response</li>
<li>Operations needing strong consistency</li>
</ul>
<p><strong>Asynchronous (Message Bus) for</strong>:</p>
<ul>
<li>Business events and notifications</li>
<li>Operations that can be eventually consistent</li>
</ul>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IInventoryService _inventoryService;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IMessageBus _messageBus;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;OrderResult&gt; <span class="hljs-title">CreateOrderAsync</span>(<span class="hljs-params">CreateOrderRequest request</span>)</span>
    {
        <span class="hljs-comment">// Sync: Check inventory (needs immediate response)</span>
        <span class="hljs-keyword">var</span> inventoryStatus = <span class="hljs-keyword">await</span> _inventoryService.CheckAvailabilityAsync(request.ProductId);
        
        <span class="hljs-keyword">if</span> (!inventoryStatus.IsAvailable)
            <span class="hljs-keyword">return</span> OrderResult.Failed(<span class="hljs-string">&quot;Product not available&quot;</span>);

        <span class="hljs-comment">// Create order first</span>
        <span class="hljs-keyword">var</span> order = <span class="hljs-keyword">await</span> CreateOrderRecordAsync(request);

        <span class="hljs-comment">// Async: Publish events for other services to handle</span>
        <span class="hljs-keyword">await</span> _messageBus.PublishAsync(<span class="hljs-keyword">new</span> OrderCreatedEvent 
        { 
            OrderId = order.Id,
            ProductId = request.ProductId,
            Quantity = request.Quantity,
            CustomerId = request.CustomerId
        });

        <span class="hljs-keyword">return</span> OrderResult.Success(order.Id);
    }
}</code></pre>
<p><strong>Lesson Learned</strong>: Don&#39;t default to all-sync or all-async. Choose the right communication pattern based on business requirements, not technical preferences.</p>
<h2 id="lesson-4-observability-can-and-39-t-be-bolted-on-later">Lesson #4: Observability Can&#39;t Be Bolted On Later</h2>
<p>We initially focused on getting features working, planning to &quot;add monitoring later.&quot; This was a mistake that cost us significant debugging time during UAT.</p>
<h3 id="what-we-should-have-done-from-day-one">What We Should Have Done From Day One</h3>
<p>Aspire gives you observability out of the box, but you need to instrument your business logic properly:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ProductService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> ActivitySource ActivitySource = <span class="hljs-keyword">new</span>(<span class="hljs-string">&quot;CatalogApi.Products&quot;</span>);
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> Counter&lt;<span class="hljs-built_in">int</span>&gt; ProductsCreated = 
        Meter.CreateCounter&lt;<span class="hljs-built_in">int</span>&gt;(<span class="hljs-string">&quot;products_created_total&quot;</span>);
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;Product&gt; <span class="hljs-title">CreateProductAsync</span>(<span class="hljs-params">CreateProductRequest request</span>)</span>
    {
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> activity = ActivitySource.StartActivity(<span class="hljs-string">&quot;CreateProduct&quot;</span>);
        activity?.SetTag(<span class="hljs-string">&quot;product.category&quot;</span>, request.Category);
        activity?.SetTag(<span class="hljs-string">&quot;product.price&quot;</span>, request.Price);

        <span class="hljs-keyword">try</span> 
        {
            <span class="hljs-keyword">var</span> product = <span class="hljs-keyword">new</span> Product 
            {
                Name = request.Name,
                Category = request.Category,
                Price = request.Price
            };

            <span class="hljs-keyword">await</span> _context.Products.AddAsync(product);
            <span class="hljs-keyword">await</span> _context.SaveChangesAsync();

            ProductsCreated.Add(<span class="hljs-number">1</span>, 
                <span class="hljs-keyword">new</span> KeyValuePair&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">object</span>&gt;(<span class="hljs-string">&quot;category&quot;</span>, request.Category));

            _logger.LogInformation(<span class="hljs-string">&quot;Product created: {ProductId} in category {Category}&quot;</span>, 
                product.Id, product.Category);

            activity?.SetStatus(ActivityStatusCode.Ok);
            <span class="hljs-keyword">return</span> product;
        }
        <span class="hljs-keyword">catch</span> (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            _logger.LogError(ex, <span class="hljs-string">&quot;Failed to create product&quot;</span>);
            <span class="hljs-keyword">throw</span>;
        }
    }
}</code></pre>
<p><strong>The Aspire Dashboard Advantage</strong>: During development, we could see all our traces, metrics, and logs in one place. No more switching between multiple tools or guessing which service was causing issues.</p>
<p><strong>Lesson Learned</strong>: Build observability into your code from the first commit. The Aspire dashboard makes local development debugging incredibly efficient, and the same instrumentation works in production.</p>
<h2 id="lesson-5-kubernetes-deployment-complexity-is-real">Lesson #5: Kubernetes Deployment Complexity is Real</h2>
<p>Moving from local Aspire development to Kubernetes production was our biggest challenge. The gap between &quot;works on my machine with Aspire&quot; and &quot;works in Kubernetes&quot; was larger than expected.</p>
<h3 id="the-configuration-management-challenge">The Configuration Management Challenge</h3>
<p>In Aspire, configuration &quot;just works&quot;:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// This works magically in Aspire</span>
builder.AddNpgsqlDbContext&lt;CatalogDbContext&gt;(<span class="hljs-string">&quot;catalogdb&quot;</span>);</code></pre>
<p>In Kubernetes, you need explicit configuration:</p>
<pre class="blog-code"><code class="hljs language-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ConfigMap</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">catalog-api-config</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">ConnectionStrings__CatalogDb:</span> <span class="hljs-string">&quot;Host=postgres-service;Port=5432;Database=catalogdb;Username=cataloguser&quot;</span>
  <span class="hljs-attr">Redis__ConnectionString:</span> <span class="hljs-string">&quot;redis-service:6379&quot;</span>
  <span class="hljs-attr">ServiceBus__ConnectionString:</span> <span class="hljs-string">&quot;Endpoint=sb://myservicebus.servicebus.windows.net/&quot;</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">catalog-api</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">catalog-api</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">myregistry.azurecr.io/catalog-api:latest</span>
        <span class="hljs-attr">envFrom:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">configMapRef:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">catalog-api-config</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">secretRef:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">catalog-api-secrets</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8080</span>
        <span class="hljs-attr">livenessProbe:</span>
          <span class="hljs-attr">httpGet:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">/health/live</span>
            <span class="hljs-attr">port:</span> <span class="hljs-number">8080</span>
          <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">30</span>
        <span class="hljs-attr">readinessProbe:</span>
          <span class="hljs-attr">httpGet:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">/health/ready</span>
            <span class="hljs-attr">port:</span> <span class="hljs-number">8080</span>
          <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">10</span></code></pre>
<h3 id="health-checks-saved-our-deployment">Health Checks Saved Our Deployment</h3>
<p>We learned the hard way that proper health checks are essential:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// In Program.cs</span>
builder.Services.AddHealthChecks()
    .AddCheck(<span class="hljs-string">&quot;self&quot;</span>, () =&gt; HealthCheckResult.Healthy())
    .AddNpgSql(connectionString, name: <span class="hljs-string">&quot;database&quot;</span>)
    .AddRedis(redisConnectionString, name: <span class="hljs-string">&quot;redis&quot;</span>)
    .AddCheck&lt;InventoryServiceHealthCheck&gt;(<span class="hljs-string">&quot;inventory-service&quot;</span>);

app.MapHealthChecks(<span class="hljs-string">&quot;/health/live&quot;</span>, <span class="hljs-keyword">new</span> HealthCheckOptions
{
    Predicate = check =&gt; check.Name == <span class="hljs-string">&quot;self&quot;</span>
});

app.MapHealthChecks(<span class="hljs-string">&quot;/health/ready&quot;</span>, <span class="hljs-keyword">new</span> HealthCheckOptions
{
    Predicate = check =&gt; check.Tags.Contains(<span class="hljs-string">&quot;ready&quot;</span>)
});</code></pre>
<p><strong>Lesson Learned</strong>: Aspire generates Kubernetes manifests, but you&#39;ll still need to understand and customize them. Don&#39;t assume the generated manifests are production-ready without review.</p>
<h2 id="lesson-6-performance-testing-early-prevents-late-surprises">Lesson #6: Performance Testing Early Prevents Late Surprises</h2>
<p>We thought our microservices would naturally be more performant than the monolith. We were wrong.</p>
<h3 id="the-latency-discovery">The Latency Discovery</h3>
<p>During load testing, we discovered that our microservices architecture had introduced significant latency:</p>
<ul>
<li><strong>Monolith</strong>: Average response time 200ms</li>
<li><strong>Our Microservices</strong>: Average response time 800ms</li>
</ul>
<p>The culprit? Network calls and serialization overhead.</p>
<h3 id="our-performance-recovery-strategy">Our Performance Recovery Strategy</h3>
<ol>
<li><strong>Caching Strategy</strong>: Implemented Redis caching at multiple levels</li>
</ol>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">CachedProductService</span> : <span class="hljs-title">IProductService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IProductService _productService;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDistributedCache _cache;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TimeSpan _cacheDuration = TimeSpan.FromMinutes(<span class="hljs-number">15</span>);

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;Product&gt; <span class="hljs-title">GetProductAsync</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> productId</span>)</span>
    {
        <span class="hljs-keyword">var</span> cacheKey = <span class="hljs-string">$&quot;product:<span class="hljs-subst">{productId}</span>&quot;</span>;
        <span class="hljs-keyword">var</span> cachedProduct = <span class="hljs-keyword">await</span> _cache.GetStringAsync(cacheKey);
        
        <span class="hljs-keyword">if</span> (cachedProduct != <span class="hljs-literal">null</span>)
            <span class="hljs-keyword">return</span> JsonSerializer.Deserialize&lt;Product&gt;(cachedProduct);

        <span class="hljs-keyword">var</span> product = <span class="hljs-keyword">await</span> _productService.GetProductAsync(productId);
        
        <span class="hljs-keyword">if</span> (product != <span class="hljs-literal">null</span>)
        {
            <span class="hljs-keyword">var</span> serializedProduct = JsonSerializer.Serialize(product);
            <span class="hljs-keyword">await</span> _cache.SetStringAsync(cacheKey, serializedProduct, 
                <span class="hljs-keyword">new</span> DistributedCacheEntryOptions 
                { 
                    AbsoluteExpirationRelativeToNow = _cacheDuration 
                });
        }

        <span class="hljs-keyword">return</span> product;
    }
}</code></pre>
<ol>
<li><strong>Connection Pooling</strong>: Properly configured database connections</li>
</ol>
<pre class="blog-code"><code class="hljs language-csharp">builder.Services.AddNpgsqlDbContext&lt;CatalogDbContext&gt;(connectionString, options =&gt;
{
    options.EnableSensitiveDataLogging(<span class="hljs-literal">false</span>);
    options.EnableServiceProviderCaching();
    options.EnableDetailedErrors(<span class="hljs-literal">false</span>);
});

<span class="hljs-comment">// Configure connection pool</span>
builder.Services.Configure&lt;NpgsqlDbContextOptionsBuilder&gt;(options =&gt;
{
    options.EnableRetryOnFailure(maxRetryCount: <span class="hljs-number">3</span>, maxRetryDelay: TimeSpan.FromSeconds(<span class="hljs-number">5</span>), <span class="hljs-literal">null</span>);
});</code></pre>
<ol>
<li><strong>Async All the Way</strong>: Eliminated blocking calls</li>
</ol>
<p><strong>Final Results</strong>: Brought average response time down to 250ms – better than the original monolith.</p>
<p><strong>Lesson Learned</strong>: Microservices don&#39;t automatically mean better performance. You need to be intentional about latency, caching, and resource utilization.</p>
<h2 id="lesson-7-the-deployment-pipeline-is-as-important-as-the-code">Lesson #7: The Deployment Pipeline is as Important as the Code</h2>
<p>Setting up a reliable CI/CD pipeline took almost as much effort as writing the application code, but it was absolutely worth it.</p>
<h3 id="our-final-pipeline-strategy">Our Final Pipeline Strategy</h3>
<pre class="blog-code"><code class="hljs language-yaml"><span class="hljs-comment"># .github/workflows/deploy-production.yml</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">Production</span> <span class="hljs-string">Deployment</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> [<span class="hljs-string">main</span>]
    <span class="hljs-attr">tags:</span> [<span class="hljs-string">&#x27;v*&#x27;</span>]

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">test:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">.NET</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-dotnet@v4</span>
      <span class="hljs-attr">with:</span>
        <span class="hljs-attr">dotnet-version:</span> <span class="hljs-string">&#x27;9.0.x&#x27;</span>
    
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        dotnet restore
        dotnet test --configuration Release --logger trx --collect:&quot;XPlat Code Coverage&quot;
</span>    
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">test</span> <span class="hljs-string">results</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v3</span>
      <span class="hljs-attr">with:</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">test-results</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">&#x27;**/*.trx&#x27;</span>

  <span class="hljs-attr">security-scan:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">security</span> <span class="hljs-string">scan</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">securecodewarrior/github-action-add-sarif@v1</span>
      <span class="hljs-attr">with:</span>
        <span class="hljs-attr">sarif-file:</span> <span class="hljs-string">&#x27;security-scan-results.sarif&#x27;</span>

  <span class="hljs-attr">build-and-deploy:</span>
    <span class="hljs-attr">needs:</span> [<span class="hljs-string">test</span>, <span class="hljs-string">security-scan</span>]
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">and</span> <span class="hljs-string">push</span> <span class="hljs-string">images</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        docker build -t $REGISTRY/catalog-api:$GITHUB_SHA .
        docker build -t $REGISTRY/inventory-api:$GITHUB_SHA .
        docker push $REGISTRY/catalog-api:$GITHUB_SHA
        docker push $REGISTRY/inventory-api:$GITHUB_SHA
</span>    
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">staging</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        kubectl set image deployment/catalog-api catalog-api=$REGISTRY/catalog-api:$GITHUB_SHA
        kubectl rollout status deployment/catalog-api
</span>    
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">smoke</span> <span class="hljs-string">tests</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|
        npm install
        npm run test:smoke
</span>    
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">production</span>
      <span class="hljs-attr">if:</span> <span class="hljs-string">startsWith(github.ref,</span> <span class="hljs-string">&#x27;refs/tags/v&#x27;</span><span class="hljs-string">)</span>
      <span class="hljs-attr">run:</span> <span class="hljs-string">|</span>
        <span class="hljs-string">kubectl</span> <span class="hljs-string">set</span> <span class="hljs-string">image</span> <span class="hljs-string">deployment/catalog-api</span> <span class="hljs-string">catalog-api=$REGISTRY/catalog-api:$GITHUB_SHA</span> <span class="hljs-string">--namespace=production</span></code></pre>
<p><strong>Lesson Learned</strong>: Invest in your deployment pipeline early. Automated testing, security scanning, and staged deployments saved us from multiple production incidents.</p>
<h2 id="what-we-and-39-d-do-differently-next-time">What We&#39;d Do Differently Next Time</h2>
<h3 id="1-start-with-aspire-service-defaults-earlier">1. Start with Aspire Service Defaults Earlier</h3>
<p>We initially wrote a lot of boilerplate configuration code that Aspire&#39;s service defaults would have handled for us:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Instead of writing all this manually...</span>
builder.Services.AddOpenTelemetry()
    .WithMetrics(...)
    .WithTracing(...);
    
builder.Services.AddServiceDiscovery();
builder.Services.AddHealthChecks();

<span class="hljs-comment">// Just use this from day one:</span>
builder.AddServiceDefaults();</code></pre>
<h3 id="2-plan-data-consistency-strategy-upfront">2. Plan Data Consistency Strategy Upfront</h3>
<p>We underestimated the complexity of managing data consistency across microservices. Implementing the saga pattern later was painful.</p>
<h3 id="3-invest-in-contract-testing">3. Invest in Contract Testing</h3>
<p>API contract changes between services caused integration issues. Tools like Pact would have caught these earlier.</p>
<h3 id="4-set-resource-limits-from-the-beginning">4. Set Resource Limits from the Beginning</h3>
<p>We experienced resource contention in Kubernetes because we didn&#39;t set proper CPU and memory limits initially.</p>
<h2 id="the-bottom-line-was-it-worth-it">The Bottom Line: Was It Worth It?</h2>
<p>Absolutely. Six months later, our client is seeing:</p>
<ul>
<li><strong>99.9% uptime</strong> (vs. 95% with the monolith)</li>
<li><strong>50% faster feature delivery</strong> (independent service deployments)</li>
<li><strong>3x better performance</strong> during peak traffic</li>
<li><strong>Reduced operational costs</strong> (better resource utilization)</li>
</ul>
<p>But more importantly, their development teams are happier and more productive.</p>
<h2 id="final-thoughts-for-fellow-consultants">Final Thoughts for Fellow Consultants</h2>
<p>Building microservices with ASP.NET Core 9 and .NET Aspire isn&#39;t just about the technology – it&#39;s about the entire ecosystem:</p>
<ol>
<li><strong>Team Readiness</strong>: Ensure your team understands distributed systems concepts</li>
<li><strong>Client Education</strong>: Help clients understand the complexity trade-offs</li>
<li><strong>Incremental Approach</strong>: Don&#39;t try to rebuild everything at once</li>
<li><strong>Observability First</strong>: Build monitoring and logging from day one</li>
<li><strong>Performance Testing</strong>: Load test early and often</li>
</ol>
<p>.NET Aspire genuinely changes the game for building cloud-native applications, but it&#39;s not a silver bullet. Success still requires solid architecture decisions, proper testing, and a team that understands distributed systems.</p>
<p>Would I recommend ASP.NET Core 9 and Aspire for similar projects? Absolutely. The developer experience improvements and production-ready defaults make it a compelling choice for teams building modern, cloud-native applications.</p>
<p>What&#39;s your experience been with ASP.NET Core 9 or .NET Aspire? I&#39;d love to hear about your lessons learned in the comments!</p>
<hr>
<p><em>Want to discuss this project or need help with your own microservices architecture? Feel free to reach out – always happy to chat about .NET, architecture, and lessons learned in the trenches.</em></p>
]]></content:encoded>
        </item>
        <item>
            <title>Mastering Entity Framework Core: A Guide to Efficient Data Access and Common Pitfalls</title>
            <link>https://www.architect4hire.com/blog/mastering-entity-framework-core-a-guide-to-efficient-data-access-and-common-pitfalls</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/mastering-entity-framework-core-a-guide-to-efficient-data-access-and-common-pitfalls</guid>
            <pubDate>Mon, 21 Jul 2025 12:18:06 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>.NET &amp; Azure</category>
            <description>A simple request for a list of blog posts ballooned into hundreds of queries. The CPU was fine. The database was fine. The problem was me — and the classic EF Core traps that catch almost everyone.</description>
            <content:encoded><![CDATA[<p>A few months ago, I found myself staring at a query that was <em>way</em> slower than it should’ve been. What started as a simple request for a list of blog posts ballooned into hundreds of queries hitting the database, one for each related author, category, and comment. My CPU was fine. My database was fine. The problem? I wasn’t.</p>
<p>Like many developers, I had fallen into some classic Entity Framework Core traps without realizing it—lazy loading, improper projections, and unoptimized query patterns. After a long night of debugging and tweaking, I finally got things running smoothly. And I figured: if I hit these issues, then others are likely to as well.</p>
<p>So I decided to write the guide I wish I had when I was pulling my hair out.</p>
<p>Entity Framework Core (EF Core) is Microsoft’s go-to object-relational mapper (ORM) for .NET developers, and for good reason. It makes data access clean, testable, and powerful. But with that power comes a whole set of performance landmines that can cripple your application if you’re not careful.</p>
<p>In this post, I’ll walk you through the most common EF Core performance pitfalls—and show you the wrong and right ways to handle them, with code examples and practical tips you can use immediately.</p>
<h2 id="understanding-ef-core-and-39-s-lazy-loading-trap">Understanding EF Core&#39;s Lazy Loading Trap</h2>
<p>One of the most notorious performance killers in EF Core applications is the N+1 query problem, often caused by lazy loading. This occurs when you retrieve a list of entities and then access related data for each entity individually.</p>
<h3 id="the-wrong-way-triggering-n-1-queries">The Wrong Way: Triggering N+1 Queries</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ❌ BAD: This will execute N+1 queries</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;BlogPostViewModel&gt;&gt; GetBlogPostsAsync()
{
    <span class="hljs-keyword">var</span> posts = <span class="hljs-keyword">await</span> _context.BlogPosts.ToListAsync();
    
    <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">new</span> List&lt;BlogPostViewModel&gt;();
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> post <span class="hljs-keyword">in</span> posts)
    {
        result.Add(<span class="hljs-keyword">new</span> BlogPostViewModel
        {
            Title = post.Title,
            Content = post.Content,
            AuthorName = post.Author.Name, <span class="hljs-comment">// Lazy loading triggers a query for each post</span>
            CategoryName = post.Category.Name, <span class="hljs-comment">// Another query for each post</span>
            CommentCount = post.Comments.Count() <span class="hljs-comment">// Yet another query for each post</span>
        });
    }
    
    <span class="hljs-keyword">return</span> result;
}</code></pre>
<p>If you have 100 blog posts, this code will execute 301 queries: 1 to get the posts, 100 to get authors, 100 to get categories, and 100 to get comment counts.</p>
<h3 id="the-right-way-using-eager-loading-with-include">The Right Way: Using Eager Loading with Include</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Using Include to load related data in a single query</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;BlogPostViewModel&gt;&gt; GetBlogPostsAsync()
{
    <span class="hljs-keyword">var</span> posts = <span class="hljs-keyword">await</span> _context.BlogPosts
        .Include(p =&gt; p.Author)
        .Include(p =&gt; p.Category)
        .Include(p =&gt; p.Comments)
        .ToListAsync();
    
    <span class="hljs-keyword">return</span> posts.Select(post =&gt; <span class="hljs-keyword">new</span> BlogPostViewModel
    {
        Title = post.Title,
        Content = post.Content,
        AuthorName = post.Author.Name,
        CategoryName = post.Category.Name,
        CommentCount = post.Comments.Count
    }).ToList();
}</code></pre>
<h3 id="even-better-using-projections-to-select-only-what-you-need">Even Better: Using Projections to Select Only What You Need</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ BETTER: Using projections to select only required data</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;BlogPostViewModel&gt;&gt; GetBlogPostsAsync()
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _context.BlogPosts
        .Select(p =&gt; <span class="hljs-keyword">new</span> BlogPostViewModel
        {
            Title = p.Title,
            Content = p.Content,
            AuthorName = p.Author.Name,
            CategoryName = p.Category.Name,
            CommentCount = p.Comments.Count()
        })
        .ToListAsync();
}</code></pre>
<p>This approach loads only the data you actually need and executes a single, optimized query.</p>
<h2 id="avoiding-the-and-quot-select-n-1-and-quot-anti-pattern-with-batch-loading">Avoiding the &quot;Select N+1&quot; Anti-Pattern with Batch Loading</h2>
<p>When you need to load related data conditionally or in complex scenarios, consider using batch loading techniques.</p>
<h3 id="the-wrong-way-individual-queries-in-loops">The Wrong Way: Individual Queries in Loops</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ❌ BAD: Loading related data individually</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;OrderSummary&gt;&gt; GetOrderSummariesAsync(List&lt;<span class="hljs-built_in">int</span>&gt; orderIds)
{
    <span class="hljs-keyword">var</span> summaries = <span class="hljs-keyword">new</span> List&lt;OrderSummary&gt;();
    
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> orderId <span class="hljs-keyword">in</span> orderIds)
    {
        <span class="hljs-keyword">var</span> order = <span class="hljs-keyword">await</span> _context.Orders
            .Include(o =&gt; o.OrderItems)
            .ThenInclude(oi =&gt; oi.Product)
            .FirstOrDefaultAsync(o =&gt; o.Id == orderId);
            
        <span class="hljs-keyword">if</span> (order != <span class="hljs-literal">null</span>)
        {
            summaries.Add(<span class="hljs-keyword">new</span> OrderSummary
            {
                OrderId = order.Id,
                TotalAmount = order.OrderItems.Sum(oi =&gt; oi.Quantity * oi.Product.Price),
                ItemCount = order.OrderItems.Count
            });
        }
    }
    
    <span class="hljs-keyword">return</span> summaries;
}</code></pre>
<h3 id="the-right-way-batch-loading">The Right Way: Batch Loading</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Loading all data in a single query</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;OrderSummary&gt;&gt; GetOrderSummariesAsync(List&lt;<span class="hljs-built_in">int</span>&gt; orderIds)
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _context.Orders
        .Where(o =&gt; orderIds.Contains(o.Id))
        .Select(o =&gt; <span class="hljs-keyword">new</span> OrderSummary
        {
            OrderId = o.Id,
            TotalAmount = o.OrderItems.Sum(oi =&gt; oi.Quantity * oi.Product.Price),
            ItemCount = o.OrderItems.Count()
        })
        .ToListAsync();
}</code></pre>
<h2 id="managing-dbcontext-lifecycle-properly">Managing DbContext Lifecycle Properly</h2>
<p>One critical aspect of EF Core that developers often get wrong is DbContext lifecycle management. The DbContext is designed to be short-lived and should not be shared across multiple operations.</p>
<h3 id="the-wrong-way-long-lived-dbcontext">The Wrong Way: Long-lived DbContext</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ❌ BAD: Keeping DbContext alive for too long</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">BadUserService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ApplicationDbContext _context;
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">BadUserService</span>(<span class="hljs-params">ApplicationDbContext context</span>)</span>
    {
        _context = context; <span class="hljs-comment">// This context might be shared across requests</span>
    }
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;User&gt; <span class="hljs-title">GetUserAsync</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> id</span>)</span>
    {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _context.Users.FindAsync(id);
    }
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">UpdateUserAsync</span>(<span class="hljs-params">User user</span>)</span>
    {
        _context.Users.Update(user);
        <span class="hljs-keyword">await</span> _context.SaveChangesAsync();
    }
}</code></pre>
<h3 id="the-right-way-proper-dbcontext-lifecycle">The Right Way: Proper DbContext Lifecycle</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Using scoped DbContext or explicit disposal</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">GoodUserService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDbContextFactory&lt;ApplicationDbContext&gt; _contextFactory;
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">GoodUserService</span>(<span class="hljs-params">IDbContextFactory&lt;ApplicationDbContext&gt; contextFactory</span>)</span>
    {
        _contextFactory = contextFactory;
    }
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;User&gt; <span class="hljs-title">GetUserAsync</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> id</span>)</span>
    {
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> context = _contextFactory.CreateDbContext();
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> context.Users.FindAsync(id);
    }
    
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">UpdateUserAsync</span>(<span class="hljs-params">User user</span>)</span>
    {
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> context = _contextFactory.CreateDbContext();
        context.Users.Update(user);
        <span class="hljs-keyword">await</span> context.SaveChangesAsync();
    }
}</code></pre>
<p>For dependency injection in ASP.NET Core, use the scoped lifetime:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// In Program.cs or Startup.cs</span>
services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;
    options.UseSqlServer(connectionString), ServiceLifetime.Scoped);</code></pre>
<h2 id="optimizing-queries-with-proper-indexing-and-query-structure">Optimizing Queries with Proper Indexing and Query Structure</h2>
<p>EF Core generates SQL queries based on your LINQ expressions, but not all LINQ expressions are created equal in terms of performance.</p>
<h3 id="the-wrong-way-inefficient-query-patterns">The Wrong Way: Inefficient Query Patterns</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ❌ BAD: Using client-side evaluation</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;Product&gt;&gt; GetExpensiveProductsAsync()
{
    <span class="hljs-keyword">var</span> products = <span class="hljs-keyword">await</span> _context.Products.ToListAsync();
    
    <span class="hljs-comment">// This calculation happens in memory, not in the database</span>
    <span class="hljs-keyword">return</span> products
        .Where(p =&gt; CalculateDiscountedPrice(p.Price, p.DiscountPercentage) &gt; <span class="hljs-number">100</span>)
        .ToList();
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-built_in">decimal</span> <span class="hljs-title">CalculateDiscountedPrice</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> price, <span class="hljs-built_in">decimal</span> discountPercentage</span>)</span>
{
    <span class="hljs-keyword">return</span> price * (<span class="hljs-number">1</span> - discountPercentage / <span class="hljs-number">100</span>);
}</code></pre>
<h3 id="the-right-way-database-side-evaluation">The Right Way: Database-side Evaluation</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Using database expressions</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;Product&gt;&gt; GetExpensiveProductsAsync()
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _context.Products
        .Where(p =&gt; p.Price * (<span class="hljs-number">1</span> - p.DiscountPercentage / <span class="hljs-number">100</span>) &gt; <span class="hljs-number">100</span>)
        .ToListAsync();
}</code></pre>
<h3 id="using-raw-sql-for-complex-queries">Using Raw SQL for Complex Queries</h3>
<p>Sometimes, complex business logic is better expressed in raw SQL:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Using raw SQL for complex aggregations</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;SalesReport&gt;&gt; GetMonthlySalesReportAsync(<span class="hljs-built_in">int</span> year)
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _context.SalesReports
        .FromSqlRaw(<span class="hljs-string">@&quot;
            SELECT 
                MONTH(o.OrderDate) as Month,
                COUNT(*) as OrderCount,
                SUM(oi.Quantity * oi.UnitPrice) as TotalRevenue
            FROM Orders o
            INNER JOIN OrderItems oi ON o.Id = oi.OrderId
            WHERE YEAR(o.OrderDate) = {0}
            GROUP BY MONTH(o.OrderDate)
            ORDER BY MONTH(o.OrderDate)&quot;</span>, year)
        .ToListAsync();
}</code></pre>
<h2 id="implementing-effective-change-tracking-strategies">Implementing Effective Change Tracking Strategies</h2>
<p>EF Core&#39;s change tracking can become a performance bottleneck when working with large datasets. Understanding when to disable it can significantly improve performance.</p>
<h3 id="the-wrong-way-always-using-change-tracking">The Wrong Way: Always Using Change Tracking</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ❌ BAD: Using change tracking for read-only operations</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;ProductReport&gt;&gt; GenerateProductReportAsync()
{
    <span class="hljs-keyword">var</span> products = <span class="hljs-keyword">await</span> _context.Products
        .Include(p =&gt; p.Category)
        .Include(p =&gt; p.OrderItems)
        .ToListAsync(); <span class="hljs-comment">// Change tracking is enabled by default</span>
    
    <span class="hljs-keyword">return</span> products.Select(p =&gt; <span class="hljs-keyword">new</span> ProductReport
    {
        ProductName = p.Name,
        CategoryName = p.Category.Name,
        TotalSold = p.OrderItems.Sum(oi =&gt; oi.Quantity),
        Revenue = p.OrderItems.Sum(oi =&gt; oi.Quantity * oi.UnitPrice)
    }).ToList();
}</code></pre>
<h3 id="the-right-way-selective-change-tracking">The Right Way: Selective Change Tracking</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Disabling change tracking for read-only operations</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;List&lt;ProductReport&gt;&gt; GenerateProductReportAsync()
{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _context.Products
        .AsNoTracking() <span class="hljs-comment">// Disable change tracking</span>
        .Select(p =&gt; <span class="hljs-keyword">new</span> ProductReport
        {
            ProductName = p.Name,
            CategoryName = p.Category.Name,
            TotalSold = p.OrderItems.Sum(oi =&gt; oi.Quantity),
            Revenue = p.OrderItems.Sum(oi =&gt; oi.Quantity * oi.UnitPrice)
        })
        .ToListAsync();
}

<span class="hljs-comment">// For updates, use change tracking selectively</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">UpdateProductPricesAsync</span>(<span class="hljs-params">List&lt;ProductPriceUpdate&gt; updates</span>)</span>
{
    <span class="hljs-keyword">var</span> productIds = updates.Select(u =&gt; u.ProductId).ToList();
    <span class="hljs-keyword">var</span> products = <span class="hljs-keyword">await</span> _context.Products
        .Where(p =&gt; productIds.Contains(p.Id))
        .ToListAsync(); <span class="hljs-comment">// Change tracking enabled for updates</span>
    
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> update <span class="hljs-keyword">in</span> updates)
    {
        <span class="hljs-keyword">var</span> product = products.First(p =&gt; p.Id == update.ProductId);
        product.Price = update.NewPrice;
        product.LastUpdated = DateTime.UtcNow;
    }
    
    <span class="hljs-keyword">await</span> _context.SaveChangesAsync();
}</code></pre>
<h2 id="implementing-efficient-bulk-operations">Implementing Efficient Bulk Operations</h2>
<p>When dealing with large datasets, individual entity operations can be extremely slow.</p>
<h3 id="the-wrong-way-individual-operations">The Wrong Way: Individual Operations</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ❌ BAD: Inserting records one by one</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">ImportUsersAsync</span>(<span class="hljs-params">List&lt;UserImportModel&gt; users</span>)</span>
{
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> userModel <span class="hljs-keyword">in</span> users)
    {
        <span class="hljs-keyword">var</span> user = <span class="hljs-keyword">new</span> User
        {
            Name = userModel.Name,
            Email = userModel.Email,
            CreatedDate = DateTime.UtcNow
        };
        
        _context.Users.Add(user);
        <span class="hljs-keyword">await</span> _context.SaveChangesAsync(); <span class="hljs-comment">// Saves after each insert</span>
    }
}</code></pre>
<h3 id="the-right-way-bulk-operations">The Right Way: Bulk Operations</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Bulk insert with batching</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">ImportUsersAsync</span>(<span class="hljs-params">List&lt;UserImportModel&gt; users</span>)</span>
{
    <span class="hljs-keyword">const</span> <span class="hljs-built_in">int</span> batchSize = <span class="hljs-number">1000</span>;
    
    <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; users.Count; i += batchSize)
    {
        <span class="hljs-keyword">var</span> batch = users.Skip(i).Take(batchSize);
        <span class="hljs-keyword">var</span> userEntities = batch.Select(u =&gt; <span class="hljs-keyword">new</span> User
        {
            Name = u.Name,
            Email = u.Email,
            CreatedDate = DateTime.UtcNow
        }).ToList();
        
        _context.Users.AddRange(userEntities);
        <span class="hljs-keyword">await</span> _context.SaveChangesAsync();
        
        <span class="hljs-comment">// Clear tracked entities to free memory</span>
        _context.ChangeTracker.Clear();
    }
}</code></pre>
<h2 id="best-practices-for-configuration-and-performance">Best Practices for Configuration and Performance</h2>
<h3 id="configure-your-dbcontext-for-performance">Configure Your DbContext for Performance</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Optimized DbContext configuration</span>
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnConfiguring</span>(<span class="hljs-params">DbContextOptionsBuilder optionsBuilder</span>)</span>
{
    optionsBuilder
        .UseSqlServer(connectionString, options =&gt;
        {
            options.CommandTimeout(<span class="hljs-number">30</span>); <span class="hljs-comment">// Set appropriate timeout</span>
            options.EnableRetryOnFailure(); <span class="hljs-comment">// Handle transient failures</span>
        })
        .EnableSensitiveDataLogging(<span class="hljs-literal">false</span>) <span class="hljs-comment">// Disable in production</span>
        .EnableDetailedErrors(<span class="hljs-literal">false</span>) <span class="hljs-comment">// Disable in production</span>
        .ConfigureWarnings(warnings =&gt;
            warnings.Throw(RelationalEventId.QueryPossibleUnintendedUseOfEqualsWarning));
}</code></pre>
<h3 id="implement-connection-resiliency">Implement Connection Resiliency</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Connection resiliency configuration</span>
services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;
{
    options.UseSqlServer(connectionString, sqlOptions =&gt;
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: <span class="hljs-number">3</span>,
            maxRetryDelay: TimeSpan.FromSeconds(<span class="hljs-number">30</span>),
            errorNumbersToAdd: <span class="hljs-literal">null</span>);
    });
});</code></pre>
<h2 id="monitoring-and-debugging-query-performance">Monitoring and Debugging Query Performance</h2>
<p>Always monitor your EF Core queries in production:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// ✅ GOOD: Logging slow queries</span>
<span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnConfiguring</span>(<span class="hljs-params">DbContextOptionsBuilder optionsBuilder</span>)</span>
{
    optionsBuilder
        .UseSqlServer(connectionString)
        .LogTo(Console.WriteLine, LogLevel.Information)
        .EnableSensitiveDataLogging() <span class="hljs-comment">// Only in development</span>
        .ConfigureWarnings(warnings =&gt;
            warnings.Log(CoreEventId.FirstWithoutOrderByAndFilterWarning,
                        CoreEventId.RowLimitingOperationWithoutOrderByWarning));
}</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>Efficient use of Entity Framework Core requires understanding its internals and being mindful of the SQL queries it generates. The key principles to remember are:</p>
<ol>
<li><strong>Always think about the SQL being generated</strong> - Use tools like EF Core&#39;s logging to see what queries are executed</li>
<li><strong>Avoid N+1 problems</strong> - Use Include, projections, or batch loading appropriately</li>
<li><strong>Manage DbContext lifecycle properly</strong> - Keep contexts short-lived and dispose them correctly</li>
<li><strong>Use AsNoTracking() for read-only operations</strong> - Don&#39;t track entities you won&#39;t modify</li>
<li><strong>Batch operations when dealing with large datasets</strong> - Avoid individual saves in loops</li>
<li><strong>Configure connection resiliency</strong> - Handle transient failures gracefully</li>
<li><strong>Monitor and profile your queries</strong> - Use logging and profiling tools to identify bottlenecks</li>
</ol>
<p>By following these practices and avoiding the common pitfalls outlined in this guide, you&#39;ll be able to build high-performance applications that scale effectively with Entity Framework Core. Remember that premature optimization can be counterproductive, so profile your application to identify actual bottlenecks before implementing complex optimizations.</p>
<p>The examples provided here represent real-world scenarios that many developers encounter. By understanding both the wrong and right approaches, you&#39;ll be better equipped to write efficient, maintainable code that performs well under load.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Supercharge Your Testing with GitHub Copilot: Generating MSTest DataRows for an Onion Architecture</title>
            <link>https://www.architect4hire.com/blog/supercharge-your-testing-with-github-copilot</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/supercharge-your-testing-with-github-copilot</guid>
            <pubDate>Fri, 11 Jul 2025 19:38:36 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>AI-Assisted Development</category>
            <description>Testing is the backbone of reliable software and the most time-consuming part of building it. Here are the prompts that get Copilot generating genuinely useful MSTest DataRows across an Onion Architecture — controller and business layers both.</description>
            <content:encoded><![CDATA[<h2 id="generating-mstest-datarows-for-an-onion-style-architecture">Generating MSTest DataRows for an Onion Style Architecture</h2>
<p>Testing is the backbone of reliable software development, yet it&#39;s often the most time-consuming aspect of building robust applications. Suppose you&#39;re working with an Onion Architecture (also known as Clean Architecture) service and need to test both your controller and business layers thoroughly. In that case, GitHub Copilot can be your secret weapon for generating comprehensive MSTest suites with DataRows that efficiently cover multiple test scenarios.</p>
<p>In this comprehensive guide, we&#39;ll explore how to leverage GitHub Copilot&#39;s AI-powered code generation to create sophisticated test suites that validate your onion architecture&#39;s controller and business layers. We&#39;ll cover practical techniques, provide real-world examples, and share expert tips to maximize your testing productivity.</p>
<h2 id="understanding-onion-architecture-testing-challenges">Understanding Onion Architecture Testing Challenges</h2>
<p>Before diving into Copilot techniques, let&#39;s briefly review the testing challenges in onion architecture:</p>
<ol>
<li><strong>Controller Layer</strong>: HTTP concerns, request/response mapping, validation, authorization</li>
<li><strong>Business Layer</strong>: Core business logic, domain rules, orchestration between services</li>
<li><strong>Data Layer</strong>: Repository patterns, data access, entity mapping</li>
</ol>
<p>Each layer requires different testing approaches, and DataRows help us test multiple scenarios without code duplication.</p>
<h2 id="setting-up-your-testing-environment">Setting Up Your Testing Environment</h2>
<p>First, ensure your test project has the necessary NuGet packages:</p>
<pre class="blog-code"><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;Microsoft.NET.Test.Sdk&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;17.6.0&quot;</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;MSTest.TestAdapter&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;3.0.4&quot;</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;MSTest.TestFramework&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;3.0.4&quot;</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;Moq&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;4.20.69&quot;</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;Microsoft.AspNetCore.Mvc.Testing&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;7.0.0&quot;</span> /&gt;</span></code></pre>
<h2 id="effective-prompts-for-controller-layer-testing">Effective Prompts for Controller Layer Testing</h2>
<h3 id="basic-controller-test-generation">Basic Controller Test Generation</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Generate MSTest with DataRows for testing a UserController PostUser action that accepts CreateUserRequest and returns UserResponse. Test cases should include valid user creation, invalid email format, missing required fields, and duplicate username scenarios. Use AAA pattern and include proper assertions.</code></pre>
<p><strong>Generated Result:</strong></p>
<pre class="blog-code"><code class="hljs language-csharp">[<span class="hljs-meta">TestClass</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">UserControllerTests</span>
{
    <span class="hljs-keyword">private</span> UserController _controller;
    <span class="hljs-keyword">private</span> Mock&lt;IUserService&gt; _mockUserService;

    [<span class="hljs-meta">TestInitialize</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Setup</span>()</span>
    {
        _mockUserService = <span class="hljs-keyword">new</span> Mock&lt;IUserService&gt;();
        _controller = <span class="hljs-keyword">new</span> UserController(_mockUserService.Object);
    }

    [<span class="hljs-meta">TestMethod</span>]
    [<span class="hljs-meta">DataRow(<span class="hljs-string">&quot;john.doe@email.com&quot;</span>, <span class="hljs-string">&quot;John&quot;</span>, <span class="hljs-string">&quot;Doe&quot;</span>, <span class="hljs-string">&quot;johndoe&quot;</span>, true, <span class="hljs-string">&quot;User created successfully&quot;</span>)</span>]
    [<span class="hljs-meta">DataRow(<span class="hljs-string">&quot;invalid-email&quot;</span>, <span class="hljs-string">&quot;John&quot;</span>, <span class="hljs-string">&quot;Doe&quot;</span>, <span class="hljs-string">&quot;johndoe&quot;</span>, false, <span class="hljs-string">&quot;Invalid email format&quot;</span>)</span>]
    [<span class="hljs-meta">DataRow(<span class="hljs-string">&quot;&quot;</span>, <span class="hljs-string">&quot;John&quot;</span>, <span class="hljs-string">&quot;Doe&quot;</span>, <span class="hljs-string">&quot;johndoe&quot;</span>, false, <span class="hljs-string">&quot;Email is required&quot;</span>)</span>]
    [<span class="hljs-meta">DataRow(<span class="hljs-string">&quot;john.doe@email.com&quot;</span>, <span class="hljs-string">&quot;&quot;</span>, <span class="hljs-string">&quot;Doe&quot;</span>, <span class="hljs-string">&quot;johndoe&quot;</span>, false, <span class="hljs-string">&quot;First name is required&quot;</span>)</span>]
    [<span class="hljs-meta">DataRow(<span class="hljs-string">&quot;john.doe@email.com&quot;</span>, <span class="hljs-string">&quot;John&quot;</span>, <span class="hljs-string">&quot;Doe&quot;</span>, <span class="hljs-string">&quot;&quot;</span>, false, <span class="hljs-string">&quot;Username is required&quot;</span>)</span>]
    [<span class="hljs-meta">DataRow(<span class="hljs-string">&quot;existing@email.com&quot;</span>, <span class="hljs-string">&quot;John&quot;</span>, <span class="hljs-string">&quot;Doe&quot;</span>, <span class="hljs-string">&quot;existinguser&quot;</span>, false, <span class="hljs-string">&quot;Username already exists&quot;</span>)</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">PostUser_WithVariousInputs_ReturnsExpectedResults</span>(<span class="hljs-params">
        <span class="hljs-built_in">string</span> email, <span class="hljs-built_in">string</span> firstName, <span class="hljs-built_in">string</span> lastName, <span class="hljs-built_in">string</span> username, 
        <span class="hljs-built_in">bool</span> shouldSucceed, <span class="hljs-built_in">string</span> expectedMessage</span>)</span>
    {
        <span class="hljs-comment">// Arrange</span>
        <span class="hljs-keyword">var</span> request = <span class="hljs-keyword">new</span> CreateUserRequest
        {
            Email = email,
            FirstName = firstName,
            LastName = lastName,
            Username = username
        };

        <span class="hljs-keyword">if</span> (shouldSucceed)
        {
            _mockUserService.Setup(x =&gt; x.CreateUserAsync(It.IsAny&lt;CreateUserRequest&gt;()))
                .ReturnsAsync(<span class="hljs-keyword">new</span> UserResponse { Id = <span class="hljs-number">1</span>, Message = expectedMessage });
        }
        <span class="hljs-keyword">else</span>
        {
            _mockUserService.Setup(x =&gt; x.CreateUserAsync(It.IsAny&lt;CreateUserRequest&gt;()))
                .ThrowsAsync(<span class="hljs-keyword">new</span> ValidationException(expectedMessage));
        }

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">var</span> result = shouldSucceed 
            ? <span class="hljs-keyword">await</span> _controller.PostUser(request) <span class="hljs-keyword">as</span> OkObjectResult
            : <span class="hljs-keyword">await</span> _controller.PostUser(request) <span class="hljs-keyword">as</span> BadRequestObjectResult;

        <span class="hljs-comment">// Assert</span>
        Assert.IsNotNull(result);
        <span class="hljs-keyword">if</span> (shouldSucceed)
        {
            <span class="hljs-keyword">var</span> response = result.Value <span class="hljs-keyword">as</span> UserResponse;
            Assert.AreEqual(expectedMessage, response.Message);
        }
        <span class="hljs-keyword">else</span>
        {
            Assert.IsTrue(result.Value.ToString().Contains(expectedMessage));
        }
    }
}</code></pre>
<h3 id="advanced-authorization-testing">Advanced Authorization Testing</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Create MSTest DataRows for testing authorization in ProductController GetProducts action. Include test cases for admin role (full access), user role (own products only), guest (no access), expired token, and invalid token scenarios. Mock IAuthorizationService and include proper HTTP status code assertions.</code></pre>
<h2 id="business-layer-testing-strategies">Business Layer Testing Strategies</h2>
<h3 id="domain-logic-validation">Domain Logic Validation</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Generate MSTest with DataRows for OrderService CalculateTotal method in business layer. Test scenarios: normal order with multiple items, order with discounts, order with tax calculations, empty order, order with invalid quantities, and order with expired promotions. Include edge cases and boundary conditions.</code></pre>
<p><strong>Generated Result:</strong></p>
<pre class="blog-code"><code class="hljs language-csharp">[<span class="hljs-meta">TestClass</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderServiceTests</span>
{
    <span class="hljs-keyword">private</span> OrderService _orderService;
    <span class="hljs-keyword">private</span> Mock&lt;IDiscountRepository&gt; _mockDiscountRepo;
    <span class="hljs-keyword">private</span> Mock&lt;ITaxCalculator&gt; _mockTaxCalculator;

    [<span class="hljs-meta">TestInitialize</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Setup</span>()</span>
    {
        _mockDiscountRepo = <span class="hljs-keyword">new</span> Mock&lt;IDiscountRepository&gt;();
        _mockTaxCalculator = <span class="hljs-keyword">new</span> Mock&lt;ITaxCalculator&gt;();
        _orderService = <span class="hljs-keyword">new</span> OrderService(_mockDiscountRepo.Object, _mockTaxCalculator.Object);
    }

    [<span class="hljs-meta">TestMethod</span>]
    [<span class="hljs-meta">DataRow(new double[</span>] { <span class="hljs-number">10.00</span>, <span class="hljs-number">20.00</span>, <span class="hljs-number">15.00</span> }, <span class="hljs-keyword">new</span> <span class="hljs-built_in">int</span>[] { <span class="hljs-number">2</span>, <span class="hljs-number">1</span>, <span class="hljs-number">3</span> }, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.10</span>, <span class="hljs-number">96.25</span>)]
    [<span class="hljs-meta">DataRow(new double[</span>] { <span class="hljs-number">100.00</span> }, <span class="hljs-keyword">new</span> <span class="hljs-built_in">int</span>[] { <span class="hljs-number">1</span> }, <span class="hljs-number">10.0</span>, <span class="hljs-number">0.08</span>, <span class="hljs-number">97.20</span>)]
    [<span class="hljs-meta">DataRow(new double[</span>] { <span class="hljs-number">50.00</span>, <span class="hljs-number">30.00</span> }, <span class="hljs-keyword">new</span> <span class="hljs-built_in">int</span>[] { <span class="hljs-number">1</span>, <span class="hljs-number">2</span> }, <span class="hljs-number">15.0</span>, <span class="hljs-number">0.12</span>, <span class="hljs-number">100.80</span>)]
    [<span class="hljs-meta">DataRow(new double[</span>] { }, <span class="hljs-keyword">new</span> <span class="hljs-built_in">int</span>[] { }, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>)]
    [<span class="hljs-meta">DataRow(new double[</span>] { <span class="hljs-number">25.00</span> }, <span class="hljs-keyword">new</span> <span class="hljs-built_in">int</span>[] { <span class="hljs-number">0</span> }, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>)]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CalculateTotal_WithVariousOrderConfigurations_ReturnsCorrectTotal</span>(<span class="hljs-params">
        <span class="hljs-built_in">double</span>[] itemPrices, <span class="hljs-built_in">int</span>[] quantities, <span class="hljs-built_in">double</span> discountPercent, 
        <span class="hljs-built_in">double</span> taxRate, <span class="hljs-built_in">double</span> expectedTotal</span>)</span>
    {
        <span class="hljs-comment">// Arrange</span>
        <span class="hljs-keyword">var</span> orderItems = <span class="hljs-keyword">new</span> List&lt;OrderItem&gt;();
        <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; itemPrices.Length; i++)
        {
            <span class="hljs-keyword">if</span> (i &lt; quantities.Length &amp;&amp; quantities[i] &gt; <span class="hljs-number">0</span>)
            {
                orderItems.Add(<span class="hljs-keyword">new</span> OrderItem 
                { 
                    Price = itemPrices[i], 
                    Quantity = quantities[i] 
                });
            }
        }

        <span class="hljs-keyword">var</span> order = <span class="hljs-keyword">new</span> Order { Items = orderItems };

        _mockDiscountRepo.Setup(x =&gt; x.GetApplicableDiscount(It.IsAny&lt;Order&gt;()))
            .Returns(discountPercent);
        _mockTaxCalculator.Setup(x =&gt; x.CalculateTax(It.IsAny&lt;<span class="hljs-built_in">double</span>&gt;()))
            .Returns&lt;<span class="hljs-built_in">double</span>&gt;(amount =&gt; amount * taxRate);

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">var</span> result = _orderService.CalculateTotal(order);

        <span class="hljs-comment">// Assert</span>
        Assert.AreEqual(expectedTotal, result, <span class="hljs-number">0.01</span>);
    }
}</code></pre>
<h3 id="complex-business-rules-testing">Complex Business Rules Testing</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Create comprehensive MSTest DataRows for InventoryService ProcessRestockOrder method. Test scenarios include: sufficient stock levels, insufficient stock with backorder, priority customer handling, seasonal restrictions, vendor availability, quantity limits, and concurrent stock updates. Use realistic business scenarios and include proper exception handling tests.</code></pre>
<h2 id="advanced-copilot-techniques">Advanced Copilot Techniques</h2>
<h3 id="using-context-aware-prompts">Using Context-Aware Prompts</h3>
<p><strong>Effective Strategy:</strong> Start your prompts with existing code context to get more accurate results.</p>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Based on this existing UserService interface:
```csharp
public interface IUserService
{
    Task&lt;UserResponse&gt; CreateUserAsync(CreateUserRequest request);
    Task&lt;UserResponse&gt; GetUserByIdAsync(int id);
    Task&lt;bool&gt; DeleteUserAsync(int id);
}</code></pre>
<p>Generate MSTest with DataRows for UserService implementation testing all methods. Include positive cases, negative cases, async exception handling, and edge cases. Mock dependencies appropriately.</p>
<pre class="blog-code"><code class="hljs language-plaintext">### Chaining Prompts for Complex Scenarios

**Initial Prompt:**</code></pre>
<p>Generate MSTest DataRows for PaymentService ProcessPayment method with basic scenarios: successful payment, insufficient funds, invalid card, expired card.</p>
<pre class="blog-code"><code class="hljs language-plaintext">**Follow-up Prompt:**</code></pre>
<p>Extend the previous PaymentService tests with additional DataRows for: fraud detection triggers, currency conversion scenarios, refund processing, partial payments, and payment gateway timeout handling.</p>
<pre class="blog-code"><code class="hljs language-plaintext">### Integration Testing with Copilot

**Prompt:**</code></pre>
<p>Create MSTest integration tests with DataRows for e-commerce checkout workflow spanning controller and business layers. Test complete user journey: add to cart, apply discount, calculate shipping, process payment, update inventory. Use TestServer and realistic data scenarios.</p>
<pre class="blog-code"><code class="hljs language-plaintext">## Best Practices and Pro Tips

### 1. Structure Your Prompts Effectively

**Good Prompt Structure:**
- Context (what you&#x27;re testing)
- Specific requirements (DataRows, scenarios)
- Technical constraints (frameworks, patterns)
- Expected outcomes (assertions, exceptions)

### 2. Leverage Copilot for Test Data Generation

**Prompt:**</code></pre>
<p>Generate realistic test data objects for e-commerce testing including: valid/invalid credit cards, international addresses, product catalogs with various categories, user profiles with different roles, and order histories with edge cases.</p>
<pre class="blog-code"><code class="hljs language-plaintext">### 3. Use Copilot for Mock Setup

**Prompt:**</code></pre>
<p>Generate Moq setup code for testing OrderController that depends on IOrderService, IPaymentService, and IInventoryService. Include different return scenarios for each dependency and proper verify calls.</p>
<pre class="blog-code"><code class="hljs language-plaintext">### 4. Exception Testing Patterns

**Prompt:**</code></pre>
<p>Create MSTest DataRows for testing exception scenarios in UserRegistrationService. Include: ArgumentNullException for null inputs, ValidationException for invalid data, DuplicateUserException for existing users, and ServiceUnavailableException for external service failures.</p>
<pre class="blog-code"><code class="hljs language-plaintext">## Complex Real-World Example

Let&#x27;s create a comprehensive test suite for an order processing system:

**Prompt:**</code></pre>
<p>Generate a complete MSTest suite with DataRows for OrderProcessingService in an e-commerce onion architecture. Include controller tests for OrderController endpoints and business layer tests for OrderService. Cover scenarios: new order creation, order status updates, inventory validation, payment processing, shipping calculations, order cancellation, refund processing, and concurrent order handling. Use realistic e-commerce data and include proper mocking of dependencies.</p>
<pre class="blog-code"><code class="hljs language-plaintext">**Generated Result Structure:**
```csharp
[TestClass]
public class OrderProcessingIntegrationTests
{
    // Controller Layer Tests
    [TestMethod]
    [DataRow(&quot;valid-order.json&quot;, 201, &quot;Order created successfully&quot;)]
    [DataRow(&quot;invalid-payment.json&quot;, 400, &quot;Payment method invalid&quot;)]
    [DataRow(&quot;insufficient-inventory.json&quot;, 409, &quot;Insufficient stock&quot;)]
    public async Task CreateOrder_WithVariousScenarios_ReturnsExpectedResults(
        string testDataFile, int expectedStatusCode, string expectedMessage)
    {
        // Implementation here
    }

    // Business Layer Tests
    [TestMethod]
    [DataRow(OrderStatus.Pending, OrderStatus.Processing, true)]
    [DataRow(OrderStatus.Processing, OrderStatus.Shipped, true)]
    [DataRow(OrderStatus.Shipped, OrderStatus.Delivered, true)]
    [DataRow(OrderStatus.Delivered, OrderStatus.Processing, false)]
    public void UpdateOrderStatus_WithStatusTransitions_ValidatesBusinessRules(
        OrderStatus currentStatus, OrderStatus newStatus, bool shouldSucceed)
    {
        // Implementation here
    }
}</code></pre>
<h2 id="debugging-and-refinement-tips">Debugging and Refinement Tips</h2>
<h3 id="1-iterative-improvement">1. Iterative Improvement</h3>
<p>Start with basic tests and refine:</p>
<p><strong>Initial Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Generate basic MSTest for UserController Login method</code></pre>
<p><strong>Refinement Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Add DataRows to the previous UserController Login test for: valid credentials, invalid password, non-existent user, locked account, expired password, and rate limiting scenarios</code></pre>
<h3 id="2-copilot-assisted-test-maintenance">2. Copilot-Assisted Test Maintenance</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Update the existing UserService tests to include new DataRows for: two-factor authentication scenarios, OAuth login, password reset workflows, and account verification steps</code></pre>
<h2 id="performance-considerations">Performance Considerations</h2>
<h3 id="1-efficient-test-data">1. Efficient Test Data</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Generate MSTest DataRows using object arrays for complex scenarios in ProductCatalogService. Optimize for test execution speed while maintaining comprehensive coverage of product search, filtering, sorting, and pagination scenarios.</code></pre>
<h3 id="2-parallel-test-execution">2. Parallel Test Execution</h3>
<p><strong>Prompt:</strong></p>
<pre class="blog-code"><code class="hljs language-plaintext">Create thread-safe MSTest DataRows for concurrent testing of OrderService methods. Include proper test isolation and avoid shared state issues.</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>GitHub Copilot transforms the traditionally tedious task of writing comprehensive test suites into an efficient, AI-assisted process. By crafting effective prompts and leveraging Copilot&#39;s understanding of testing patterns, you can generate robust MSTest suites with DataRows that thoroughly validate both your controller and business layers.</p>
<p>The key to success lies in:</p>
<ol>
<li><strong>Clear, contextual prompting</strong> that provides Copilot with sufficient information</li>
<li><strong>Iterative refinement</strong> of generated tests to match your specific requirements</li>
<li><strong>Strategic use of DataRows</strong> to maximize test coverage with minimal code duplication</li>
<li><strong>Proper separation of concerns</strong> between controller and business layer tests</li>
</ol>
<p>Remember that while Copilot excels at generating boilerplate test code and common patterns, you should always review and customize the generated tests to ensure they align with your specific business requirements and testing standards.</p>
<p>Start implementing these techniques in your next project, and you&#39;ll find that comprehensive testing becomes not just manageable, but genuinely productive and even enjoyable. Your onion architecture will be more robust, your code more reliable, and your development velocity significantly increased.</p>
<p>Happy testing with GitHub Copilot!</p>
]]></content:encoded>
        </item>
        <item>
            <title>Streamlining Microservice Communication: How Refit HTTP Clients Power Our Distributed Architecture</title>
            <link>https://www.architect4hire.com/blog/streamlining-microservice-communication-how-refit-http-clients-power-our-distributed-architecture</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/streamlining-microservice-communication-how-refit-http-clients-power-our-distributed-architecture</guid>
            <pubDate>Fri, 11 Jul 2025 15:30:25 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Architecture &amp; Patterns</category>
            <description>Microservices are the easy part. Making them talk to each other without a pile of hand-rolled HttpClient boilerplate is the part that quietly rots. Refit turns inter-service communication into a declarative interface — here is how we wire it.</description>
            <content:encoded><![CDATA[<p>In the world of modern software development, microservices architecture has become the gold standard for building scalable, maintainable applications. However, with this architectural approach comes the challenge of seamless inter-service communication. Today, I want to share how we&#39;ve leveraged Refit, a powerful REST library for .NET, to create a robust and elegant HTTP client infrastructure that forms the backbone of our clients platform.</p>
<h2 id="what-is-refit-and-why-we-chose-it">What is Refit and Why We Chose It</h2>
<p>Refit is a REST library for .NET that transforms REST APIs into live interfaces. Instead of writing boilerplate HTTP client code, Refit generates the implementation for you at runtime using attributes to define the HTTP operations. This approach dramatically reduces code complexity while providing type safety and excellent IntelliSense support.</p>
<p>Our platform consists of multiple specialized services: ProfileService, UserService, AdminService, BuddyService, CheckedOutService, and several others. Each service needs to communicate with others to fulfill complex business requirements. Traditional HttpClient usage would result in hundreds of lines of repetitive code, but Refit transforms this into clean, declarative interfaces.</p>
<h2 id="the-refit-http-client-portfolio">The Refit HTTP Client Portfolio</h2>
<p>In our solution, we&#39;ve implemented a comprehensive suite of HTTP client interfaces, each designed for specific inter-service communication patterns. Let&#39;s explore the key clients that power our ecosystem:</p>
<h3 id="core-service-clients">Core Service Clients</h3>
<p><strong>IProfileHttpClient</strong> serves as the central communication hub for profile-related operations. This client handles everything from profile creation and updates to user status management:</p>
<pre class="blog-code"><code class="hljs language-csharp">[<span class="hljs-meta">Headers(<span class="hljs-string">&quot;Content-Type: application/json&quot;</span>, <span class="hljs-string">&quot;Another-Header: AnotherValue&quot;</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IProfileHttpClient</span>
{
    [<span class="hljs-meta">Post(<span class="hljs-string">&quot;/api/profile/v1/integration/profile/upsert/{userName}/{email}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;ProfileServiceModel&gt;&gt; UpsertDefaultProfile(<span class="hljs-built_in">string</span> userName, <span class="hljs-built_in">string</span> email, <span class="hljs-built_in">int</span> site);

    [<span class="hljs-meta">Post(<span class="hljs-string">&quot;/api/profile/v1/integration/profile/markloggedoff/{userGuid}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;NoResponseServiceModel&gt;&gt; MarkUserAsLoggedOff(Guid userGuid, <span class="hljs-built_in">int</span> site);

    [<span class="hljs-meta">Get(<span class="hljs-string">&quot;/api/profile/v1/integration/honeypot/{userGuid}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;HoneyPotServiceModel&gt;&gt; GetHoneypot(Guid userGuid, <span class="hljs-built_in">int</span> site);
}</code></pre>
<p>Notice how clean and readable this interface is. Each method clearly defines its HTTP verb, route template, and expected response type. The route parameters are automatically bound from method parameters, making the client both intuitive to use and resistant to errors.</p>
<p><strong>ITokenHttpClient</strong> handles authentication token management across services:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">ITokenHttpClient</span>
{
    [<span class="hljs-meta">Post(<span class="hljs-string">&quot;/api/token/v1/token/&quot;</span>)</span>]
    Task&lt;Result&lt;TokenServiceModel&gt;&gt; FetchUserServiceToken([FromBody] TokenViewModel model);
}</code></pre>
<p>This client is crucial for our authentication flow, providing service-to-service token exchange capabilities that maintain security boundaries while enabling necessary inter-service communication.</p>
<h3 id="specialized-domain-clients">Specialized Domain Clients</h3>
<p><strong>IBuddyHttpClient</strong> manages relationship and social interaction features:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IBuddyHttpClient</span>
{
    [<span class="hljs-meta">Get(<span class="hljs-string">&quot;/api/buddy/v1/integration/{userGuid}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;BuddyListServiceModel&gt;&gt; GetBuddies(Guid userGuid, <span class="hljs-built_in">int</span> site);

    [<span class="hljs-meta">Get(<span class="hljs-string">&quot;/api/buddy/v1/relationships/{userGuid}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;BuddyListServiceModel&gt;&gt; GetRelationships(Guid userGuid, <span class="hljs-built_in">int</span> site);

    [<span class="hljs-meta">Get(<span class="hljs-string">&quot;/api/buddy/v1/endorsements/{userGuid}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;EndorsementListServiceModel&gt;&gt; GetEndorsements(Guid userGuid, <span class="hljs-built_in">int</span> site);
}</code></pre>
<p><strong>ICheckedOutHttpClient</strong> handles profile viewing analytics:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">ICheckedOutHttpClient</span>
{
    [<span class="hljs-meta">Post(<span class="hljs-string">&quot;/api/checkedout/v1/integration/{userGuid}/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;ProfileViewCountServiceModel&gt;&gt; ProfileViewCountAsync(Guid userGuid, <span class="hljs-built_in">int</span> site);

    [<span class="hljs-meta">Post(<span class="hljs-string">&quot;/api/checkedout/v1/integration/{site}&quot;</span>)</span>]
    Task&lt;Result&lt;GuidListServiceModel&gt;&gt; GetMostPopularProfileViews(<span class="hljs-built_in">int</span> site);
}</code></pre>
<p>These specialized clients demonstrate how Refit enables us to create focused, single-responsibility interfaces that align perfectly with our domain boundaries.</p>
<h2 id="architectural-integration-and-service-registration">Architectural Integration and Service Registration</h2>
<p>The magic of our Refit implementation lies not just in the interface definitions, but in how we integrate them into our dependency injection container. Each service registers its required HTTP clients during startup through dedicated <code>AddHttpClients</code> methods:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">AddHttpClients</span>(<span class="hljs-params">WebApplicationBuilder builder</span>)</span>
{
    builder.Services.AddRefitClient&lt;ISocialAdHttpClient&gt;()
           .ConfigureHttpClient(c =&gt; c.BaseAddress = <span class="hljs-keyword">new</span> Uri(builder.Environment.EnvironmentName != <span class="hljs-string">&quot;LocalDevelopment&quot;</span> ? builder.Configuration[<span class="hljs-string">&quot;socialadurl&quot;</span>] : <span class="hljs-string">&quot;https+http://socialadservice&quot;</span>))
           .AddHttpMessageHandler(() =&gt; <span class="hljs-keyword">new</span> AuthHeaderHandler(builder.Services.BuildServiceProvider()));

    builder.Services.AddRefitClient&lt;IImagesHttpClient&gt;()
        .ConfigureHttpClient(c =&gt; c.BaseAddress = <span class="hljs-keyword">new</span> Uri(builder.Environment.EnvironmentName != <span class="hljs-string">&quot;LocalDevelopment&quot;</span> ? builder.Configuration[<span class="hljs-string">&quot;imagesurl&quot;</span>] : <span class="hljs-string">&quot;https+http://imagesservice&quot;</span>))
        .AddHttpMessageHandler(() =&gt; <span class="hljs-keyword">new</span> AuthHeaderHandler(builder.Services.BuildServiceProvider()));
}</code></pre>
<p>This configuration pattern provides several key benefits:</p>
<ol>
<li><strong>Environment-Aware Routing</strong>: Our services automatically switch between external URLs for production and local service discovery URLs for development</li>
<li><strong>Automatic Authentication</strong>: Most clients include our custom <code>AuthHeaderHandler</code> for seamless token management</li>
<li><strong>Centralized Configuration</strong>: All HTTP client settings are managed in one location per service</li>
</ol>
<h2 id="authentication-and-security-through-http-message-handlers">Authentication and Security Through HTTP Message Handlers</h2>
<p>One of the most powerful features of our Refit implementation is the integration with custom HTTP message handlers. Our <code>AuthHeaderHandler</code> demonstrates how cross-cutting concerns like authentication can be elegantly handled:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">AuthHeaderHandler</span> : <span class="hljs-title">DelegatingHandler</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IServiceProvider _serviceProvider;

    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task&lt;HttpResponseMessage&gt; <span class="hljs-title">SendAsync</span>(<span class="hljs-params">HttpRequestMessage request, CancellationToken cancellationToken</span>)</span>
    {
        <span class="hljs-keyword">var</span> userHttpClient = _serviceProvider.GetRequiredService&lt;ITokenHttpClient&gt;();
        <span class="hljs-keyword">var</span> configuration = _serviceProvider.GetRequiredService&lt;IConfiguration&gt;();

        <span class="hljs-keyword">var</span> tokenViewModel = <span class="hljs-keyword">new</span> TokenViewModel
        {
            Token = configuration[<span class="hljs-string">&quot;UserServiceToken&quot;</span>]
        };
        <span class="hljs-keyword">var</span> tokenResult = <span class="hljs-keyword">await</span> userHttpClient.FetchUserServiceToken(tokenViewModel);
        <span class="hljs-keyword">var</span> token = tokenResult?.Data?.Token;

        <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">string</span>.IsNullOrEmpty(token))
        {
            request.Headers.Authorization = <span class="hljs-keyword">new</span> System.Net.Http.Headers.AuthenticationHeaderValue(<span class="hljs-string">&quot;Bearer&quot;</span>, token);
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">base</span>.SendAsync(request, cancellationToken);
    }
}</code></pre>
<p>This handler automatically:</p>
<ul>
<li>Fetches authentication tokens when needed</li>
<li>Adds Bearer token headers to outgoing requests</li>
<li>Handles token refresh scenarios</li>
<li>Provides a consistent authentication experience across all services</li>
</ul>
<h2 id="multi-environment-configuration-strategy">Multi-Environment Configuration Strategy</h2>
<p>Our Refit configuration showcases a sophisticated approach to environment management. Notice how we handle different environments:</p>
<pre class="blog-code"><code class="hljs language-csharp">builder.Environment.EnvironmentName != <span class="hljs-string">&quot;LocalDevelopment&quot;</span> ? 
    builder.Configuration[<span class="hljs-string">&quot;profileurl&quot;</span>] : 
    <span class="hljs-string">&quot;https+http://profileservice&quot;</span></code></pre>
<p>In local development, we use Aspire&#39;s service discovery with the <code>https+http://</code> scheme, which automatically resolves to the correct local service endpoints. In production and staging environments, we use configuration-driven URLs that point to the appropriate service instances.</p>
<p>This approach provides several advantages:</p>
<ul>
<li>Seamless local development experience</li>
<li>Easy deployment across multiple environments</li>
<li>No code changes required between environments</li>
<li>Built-in support for service discovery and load balancing</li>
</ul>
<h2 id="error-handling-and-result-patterns">Error Handling and Result Patterns</h2>
<p>All our Refit clients return a consistent <code>Result</code> pattern that encapsulates both success and failure scenarios:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Result</span>&lt;<span class="hljs-title">T</span>&gt; <span class="hljs-keyword">where</span> <span class="hljs-title">T</span> : <span class="hljs-title">ServiceBaseModel</span> 
{
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> IsSuccess { <span class="hljs-keyword">get</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> IsFailure { <span class="hljs-keyword">get</span>; }
    <span class="hljs-keyword">public</span> List&lt;<span class="hljs-built_in">string</span>&gt; Messages { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> Error Error { <span class="hljs-keyword">get</span>; }
    <span class="hljs-keyword">public</span> T? Data { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}</code></pre>
<p>This pattern ensures that:</p>
<ul>
<li>All API responses follow a consistent structure</li>
<li>Error handling is standardized across services</li>
<li>Consumers can easily distinguish between success and failure cases</li>
<li>Rich error information is available for debugging and user feedback</li>
</ul>
<h2 id="performance-and-scalability-considerations">Performance and Scalability Considerations</h2>
<p>Our Refit implementation includes several performance optimizations:</p>
<ol>
<li><strong>HTTP Client Factory Integration</strong>: All Refit clients are registered using the <code>AddRefitClient</code> extension, which leverages the built-in HTTP client factory for connection pooling and lifecycle management</li>
<li><strong>Async/Await Throughout</strong>: Every client method returns <code>Task</code>, ensuring non-blocking I/O operations</li>
<li><strong>Efficient Serialization</strong>: Refit uses System.Text.Json by default, providing excellent performance for our JSON-based APIs</li>
<li><strong>Connection Reuse</strong>: The underlying HttpClientFactory ensures proper connection pooling and DNS refresh cycles</li>
</ol>
<h2 id="integration-with-net-aspire">Integration with .NET Aspire</h2>
<p>Our solution leverages .NET Aspire for cloud-native development, and Refit integrates seamlessly with this ecosystem. The <code>builder.AddServiceDefaults()</code> call in each service includes:</p>
<ul>
<li><strong>Service Discovery</strong>: Automatic resolution of service endpoints</li>
<li><strong>Resilience Patterns</strong>: Built-in retry policies and circuit breakers</li>
<li><strong>Observability</strong>: Automatic OpenTelemetry instrumentation for HTTP calls</li>
<li><strong>Health Checks</strong>: Integrated health monitoring for all HTTP dependencies</li>
</ul>
<h2 id="real-world-usage-patterns">Real-World Usage Patterns</h2>
<p>Let&#39;s examine how these clients are used in practice. In our ProfileService, we inject multiple HTTP clients to gather comprehensive profile information:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ProfileBusiness</span> : <span class="hljs-title">IProfileBusiness</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IBuddyHttpClient _buddyHttpClient;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ICheckedOutHttpClient _checkedOutHttpClient;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IImagesHttpClient _imagesHttpClient;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;ProfileViewModel&gt; <span class="hljs-title">GetCompleteProfile</span>(<span class="hljs-params">Guid userGuid, Site site</span>)</span>
    {
        <span class="hljs-keyword">var</span> profileTask = GetBaseProfile(userGuid, site);
        <span class="hljs-keyword">var</span> buddiesTask = _buddyHttpClient.GetBuddies(userGuid, (<span class="hljs-built_in">int</span>)site);
        <span class="hljs-keyword">var</span> viewsTask = _checkedOutHttpClient.ProfileViewCountAsync(userGuid, (<span class="hljs-built_in">int</span>)site);
        <span class="hljs-keyword">var</span> imagesTask = _imagesHttpClient.GetImages(userGuid, (<span class="hljs-built_in">int</span>)site);

        <span class="hljs-keyword">await</span> Task.WhenAll(profileTask, buddiesTask, viewsTask, imagesTask);

        <span class="hljs-keyword">return</span> BuildCompleteProfile(
            profileTask.Result,
            buddiesTask.Result,
            viewsTask.Result,
            imagesTask.Result
        );
    }
}</code></pre>
<p>This pattern allows us to make parallel HTTP calls to multiple services, dramatically improving response times while maintaining clean, readable code.</p>
<h2 id="benefits-and-lessons-learned">Benefits and Lessons Learned</h2>
<p>After implementing Refit across our entire microservices ecosystem, we&#39;ve observed several key benefits:</p>
<p><strong>Developer Productivity</strong>: Refit eliminated hundreds of lines of boilerplate HTTP client code. New developers can quickly understand and extend API integrations without learning complex HTTP client patterns.</p>
<p><strong>Type Safety</strong>: Compile-time checking ensures that API contracts are maintained. When APIs change, the compiler immediately identifies affected clients.</p>
<p><strong>Maintainability</strong>: Changes to API endpoints require updates only to interface definitions. The generated implementation handles all the underlying HTTP communication details.</p>
<p><strong>Testing</strong>: Refit interfaces are easily mockable, enabling comprehensive unit testing of service interactions without complex HTTP mocking frameworks.</p>
<p><strong>Consistency</strong>: All inter-service communication follows identical patterns, reducing cognitive load when working across different parts of the system.</p>
<h2 id="future-enhancements">Future Enhancements</h2>
<p>Looking ahead, we&#39;re exploring several enhancements to our Refit implementation:</p>
<ol>
<li><strong>Automatic Retry Policies</strong>: Integration with Polly for sophisticated retry and circuit breaker patterns</li>
<li><strong>Response Caching</strong>: Adding HTTP-level caching for appropriate GET operations</li>
<li><strong>Request/Response Logging</strong>: Enhanced observability for debugging and performance monitoring</li>
<li><strong>API Versioning</strong>: Support for multiple API versions within the same client interface</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>Refit has transformed how we handle inter-service communication in our platform. By providing a clean, type-safe abstraction over HTTP operations, it has enabled us to build a robust microservices architecture that is both maintainable and performant.</p>
<p>The combination of declarative interfaces, automatic serialization, integrated authentication, and seamless dependency injection makes Refit an invaluable tool for any .NET microservices architecture. Our implementation demonstrates how thoughtful architectural decisions can dramatically improve developer productivity while maintaining the flexibility and scalability that modern applications demand.</p>
<p>Whether you&#39;re building your first microservices system or looking to modernize an existing one, consider how Refit can simplify your HTTP client infrastructure and enable your team to focus on delivering business value rather than managing communication plumbing.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Building Modern, Observable .NET Applications: A Deep Dive into OpenTelemetry, Scalar, and OpenAPI Integration</title>
            <link>https://www.architect4hire.com/blog/building-modern-observable-net-applications-a-deep-dive-into-opentelemetry-scalar-and-openapi-integration</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/building-modern-observable-net-applications-a-deep-dive-into-opentelemetry-scalar-and-openapi-integration</guid>
            <pubDate>Fri, 11 Jul 2025 15:29:36 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>.NET &amp; Azure</category>
            <description>When you are running multiple microservices across environments, &quot;it works on my machine&quot; stops being a joke and starts being an outage. Observability cannot be bolted on later — so here is how we build it in from the start.</description>
            <content:encoded><![CDATA[<p>In today&#39;s distributed application landscape, observability isn&#39;t just a nice-to-have—it&#39;s an absolute necessity. When you&#39;re running multiple microservices across different environments, understanding what&#39;s happening inside your applications becomes critical for maintaining performance, debugging issues, and ensuring reliability. In this article, I&#39;ll walk you through how we&#39;ve integrated OpenTelemetry, Scalar, and OpenAPI technologies into our .NET 9 solution to create a comprehensive observability and documentation strategy that scales with our microservices architecture.</p>
<h2 id="the-foundation-net-aspire-and-service-defaults">The Foundation: .NET Aspire and Service Defaults</h2>
<p>Our journey begins with .NET Aspire, Microsoft&#39;s opinionated stack for building observable, production-ready cloud applications. At the heart of our implementation lies the <code>ServiceDefaults</code> project—a centralized configuration hub that standardizes observability across all our microservices.</p>
<p>The <code>ServiceDefaults</code> project serves as our single source of truth for common configurations. Every service in our solution references this project, ensuring consistent telemetry collection, health checks, and service discovery patterns. This approach eliminates configuration drift and makes it incredibly easy to add new services that automatically inherit our observability standards.</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> TBuilder <span class="hljs-title">AddServiceDefaults</span>&lt;<span class="hljs-title">TBuilder</span>&gt;(<span class="hljs-params"><span class="hljs-keyword">this</span> TBuilder builder</span>) 
    <span class="hljs-keyword">where</span> TBuilder : IHostApplicationBuilder</span>
{
    builder.ConfigureOpenTelemetry();
    builder.AddDefaultHealthChecks();
    builder.Services.AddServiceDiscovery();

    builder.Services.ConfigureHttpClientDefaults(http =&gt;
    {
        http.AddStandardResilienceHandler();
        http.AddServiceDiscovery();
    });

    <span class="hljs-keyword">return</span> builder;
}</code></pre>
<p>This extension method is called in every service&#39;s <code>Program.cs</code>, immediately providing OpenTelemetry instrumentation, health checks, and service discovery capabilities. It&#39;s a perfect example of how .NET Aspire promotes best practices by making the right thing the easy thing.</p>
<h2 id="opentelemetry-the-observability-backbone">OpenTelemetry: The Observability Backbone</h2>
<p>OpenTelemetry forms the cornerstone of our observability strategy. Rather than relying on proprietary monitoring solutions that lock us into specific vendors, we&#39;ve embraced the open standard that provides vendor-neutral instrumentation for metrics, logs, and traces.</p>
<h3 id="comprehensive-instrumentation-strategy">Comprehensive Instrumentation Strategy</h3>
<p>Our OpenTelemetry configuration captures three critical signals:</p>
<p><strong>Tracing</strong> helps us understand request flows across our distributed system. We instrument ASP.NET Core applications to automatically capture incoming HTTP requests, and we track outgoing HTTP client calls to understand inter-service dependencies. Each trace creates a complete picture of how a user request travels through our system, making it invaluable for debugging performance bottlenecks and understanding system behavior.</p>
<p><strong>Metrics</strong> provide the quantitative data we need for monitoring system health. We collect ASP.NET Core metrics for request rates and response times, HTTP client metrics for outbound call performance, and .NET runtime metrics for garbage collection and memory usage patterns. This gives us both application-level and infrastructure-level insights.</p>
<p><strong>Logging</strong> integrates seamlessly with .NET&#39;s built-in logging framework. OpenTelemetry automatically correlates log entries with traces, so when we&#39;re debugging an issue, we can see exactly which log entries belong to a specific request trace.</p>
<pre class="blog-code"><code class="hljs language-csharp">builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =&gt;
    {
        metrics.AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddRuntimeInstrumentation();
    })
    .WithTracing(tracing =&gt;
    {
        tracing.AddSource(builder.Environment.ApplicationName)
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation();
    });</code></pre>
<h3 id="flexible-export-strategy">Flexible Export Strategy</h3>
<p>One of OpenTelemetry&#39;s greatest strengths is its vendor-neutral approach to data export. Our configuration supports multiple export targets depending on the environment and requirements:</p>
<p>For development and testing environments, we can export to any OTLP-compatible endpoint by setting the <code>OTEL_EXPORTER_OTLP_ENDPOINT</code> environment variable. This might be a local Jaeger instance, a Grafana stack, or any other OpenTelemetry-compatible tool.</p>
<p>For production workloads, we integrate with Azure Monitor, leveraging Microsoft&#39;s OpenTelemetry distro to send telemetry data directly to Application Insights. This provides us with enterprise-grade monitoring capabilities while maintaining the flexibility to switch monitoring backends if needed.</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> TBuilder <span class="hljs-title">AddOpenTelemetryExporters</span>&lt;<span class="hljs-title">TBuilder</span>&gt;(<span class="hljs-params"><span class="hljs-keyword">this</span> TBuilder builder</span>) 
    <span class="hljs-keyword">where</span> TBuilder : IHostApplicationBuilder</span>
{
    <span class="hljs-keyword">var</span> useOtlpExporter = !<span class="hljs-built_in">string</span>.IsNullOrWhiteSpace(
        builder.Configuration[<span class="hljs-string">&quot;OTEL_EXPORTER_OTLP_ENDPOINT&quot;</span>]);

    <span class="hljs-keyword">if</span> (useOtlpExporter)
    {
        builder.Services.AddOpenTelemetry().UseOtlpExporter();
    }

    <span class="hljs-keyword">if</span>(builder.Environment.IsDevelopment() || builder.Environment.IsProduction())
    {
        <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">string</span>.IsNullOrEmpty(
            builder.Configuration[<span class="hljs-string">&quot;APPLICATIONINSIGHTS_CONNECTION_STRING&quot;</span>]))
        {
            builder.Services.AddOpenTelemetry().UseAzureMonitor();
        }
    }

    <span class="hljs-keyword">return</span> builder;
}</code></pre>
<p>This dual-export capability means our development teams can use lightweight, local observability tools during development while production systems benefit from Azure&#39;s robust monitoring infrastructure.</p>
<h3 id="custom-instrumentation-through-action-filters">Custom Instrumentation Through Action Filters</h3>
<p>Beyond automatic instrumentation, we&#39;ve implemented custom telemetry collection through our <code>ActionControllerLogging</code> filter. This filter captures detailed audit information for every API call, including user context, session information, request bodies, and routing data.</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ActionControllerLogging</span> : <span class="hljs-title">IAsyncActionFilter</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">OnActionExecutionAsync</span>(<span class="hljs-params">ActionExecutingContext context, ActionExecutionDelegate next</span>)</span>
    {
        HttpAuditEntryMessageModel msg = <span class="hljs-keyword">new</span> HttpAuditEntryMessageModel();

        msg.UserName = context.HttpContext.Request.Headers
            .Where(w =&gt; w.Key.ToLower() == <span class="hljs-string">&quot;username&quot;</span>).FirstOrDefault().Value;
        msg.Controller = ((ControllerBase)context.Controller)
            .ControllerContext.ActionDescriptor.ControllerName;
        msg.ActionMethod = ((ControllerBase)context.Controller)
            .ControllerContext.ActionDescriptor.ActionName;

        <span class="hljs-comment">// Additional audit data collection...</span>

        <span class="hljs-keyword">await</span> next();
    }
}</code></pre>
<p>This filter demonstrates how OpenTelemetry&#39;s extensible nature allows us to capture domain-specific telemetry that goes beyond standard web framework metrics. The audit data flows through Azure Service Bus to dedicated audit storage, creating a complete audit trail for compliance and security analysis.</p>
<h2 id="scalar-modern-api-documentation">Scalar: Modern API Documentation</h2>
<p>While OpenTelemetry handles observability, Scalar addresses another critical need: comprehensive, developer-friendly API documentation. Traditional Swagger UI, while functional, often feels outdated and lacks the polish that modern development teams expect. Scalar provides a beautiful, responsive interface that makes API exploration intuitive and enjoyable.</p>
<h3 id="seamless-integration-with-openapi">Seamless Integration with OpenAPI</h3>
<p>Our Scalar integration builds on .NET 9&#39;s native OpenAPI support. The <code>Microsoft.AspNetCore.OpenApi</code> package automatically generates OpenAPI specifications from our controller definitions, and Scalar consumes these specifications to create interactive documentation.</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> WebApplication <span class="hljs-title">ConfigureOpenApiAndScaler</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> WebApplication app</span>)</span>
{
    app.MapOpenApi();
    app.MapScalarApiReference(options =&gt;
    {
        options.WithPreferredScheme(<span class="hljs-string">&quot;Bearer&quot;</span>)
            .WithHttpBearerAuthentication(bearer =&gt;
            {
                bearer.Token = <span class="hljs-string">&quot;your-bearer-token&quot;</span>;
            });
        options.Title = <span class="hljs-string">$&quot;<span class="hljs-subst">{app.Environment.EnvironmentName}</span> - <span class="hljs-subst">{app.Environment.ApplicationName}</span>&quot;</span>;
    });
    <span class="hljs-keyword">return</span> app;
}</code></pre>
<p>This configuration provides several key benefits. The documentation automatically stays in sync with our code—when we add new endpoints or modify existing ones, the documentation updates automatically. Scalar&#39;s interface allows developers to test APIs directly from the documentation, eliminating the need to switch between documentation and testing tools.</p>
<h3 id="security-aware-documentation">Security-Aware Documentation</h3>
<p>Our Scalar configuration includes built-in authentication support. The Bearer token authentication is pre-configured, so developers can easily test secured endpoints. In development environments, this dramatically reduces the friction involved in API testing and exploration.</p>
<p>The environment-aware titles (<code>{app.Environment.EnvironmentName} - {app.Environment.ApplicationName}</code>) help developers immediately understand which environment and service they&#39;re working with, reducing confusion when working across multiple environments.</p>
<h3 id="strategic-documentation-deployment">Strategic Documentation Deployment</h3>
<p>We only expose Scalar documentation in non-production environments, maintaining security best practices while maximizing developer productivity:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-keyword">if</span> (!app.Environment.IsProduction())
{
    app.ConfigureOpenApiAndScaler();
}</code></pre>
<p>This approach ensures that our production endpoints remain secure while development and staging environments provide rich documentation experiences for both internal teams and external API consumers.</p>
<h2 id="openapi-the-contract-first-approach">OpenAPI: The Contract-First Approach</h2>
<p>OpenAPI serves as the foundation for both our documentation and our inter-service communication strategy. By leveraging .NET 9&#39;s built-in OpenAPI support, we ensure that our API contracts are automatically generated, accurate, and versioned.</p>
<h3 id="api-versioning-strategy">API Versioning Strategy</h3>
<p>Our solution implements comprehensive API versioning through the <code>Asp.Versioning</code> libraries. This allows us to evolve our APIs while maintaining backward compatibility for existing consumers.</p>
<pre class="blog-code"><code class="hljs language-csharp">builder.Services.AddApiVersioning(options =&gt;
{
    options.DefaultApiVersion = <span class="hljs-keyword">new</span> ApiVersion(primaryVersion, <span class="hljs-number">0</span>);
    options.ReportApiVersions = <span class="hljs-literal">true</span>;
    options.AssumeDefaultVersionWhenUnspecified = <span class="hljs-literal">true</span>;
    options.ApiVersionReader = ApiVersionReader.Combine(
        <span class="hljs-keyword">new</span> UrlSegmentApiVersionReader(),
        <span class="hljs-keyword">new</span> HeaderApiVersionReader(<span class="hljs-string">&quot;x-api-version&quot;</span>),
        <span class="hljs-keyword">new</span> QueryStringApiVersionReader(<span class="hljs-string">&quot;api-version&quot;</span>)
    );
});</code></pre>
<p>This configuration supports multiple versioning strategies—URL segments, headers, and query strings—giving API consumers flexibility in how they specify version requirements. The <code>ReportApiVersions</code> setting ensures that API responses include version information, helping consumers understand what versions are available.</p>
<h3 id="code-generation-and-type-safety">Code Generation and Type Safety</h3>
<p>While not explicitly shown in our current codebase, our OpenAPI specifications enable powerful code generation scenarios. Teams can generate strongly-typed HTTP clients using tools like Refit (which we include in our shared project) or NSwag, ensuring that inter-service communication is type-safe and automatically stays in sync with API changes.</p>
<p>The presence of Refit in our dependencies suggests we&#39;re using this approach for HTTP client generation:</p>
<pre class="blog-code"><code class="hljs language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;Refit&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;8.0.0&quot;</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">&quot;Refit.HttpClientFactory&quot;</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">&quot;8.0.0&quot;</span> /&gt;</span></code></pre>
<p>This enables us to define HTTP APIs as C# interfaces and let Refit handle the HTTP implementation details, creating a more maintainable and less error-prone approach to service communication.</p>
<h2 id="architectural-integration-how-it-all-works-together">Architectural Integration: How It All Works Together</h2>
<p>The true power of our observability and documentation strategy emerges from how these technologies work together to support our microservices architecture.</p>
<h3 id="service-to-service-communication">Service-to-Service Communication</h3>
<p>When a request flows through our system, OpenTelemetry automatic instrumentation creates a distributed trace that spans multiple services. If our User Service calls the Profile Service, which then calls the Image Service, we get a complete trace showing the entire request flow, including timing information for each service call.</p>
<p>The OpenAPI specifications for each service define clear contracts, while Refit-generated clients provide type-safe communication. If the Profile Service needs to call the User Service, it uses a strongly-typed interface generated from the User Service&#39;s OpenAPI specification.</p>
<h3 id="development-workflow">Development Workflow</h3>
<p>During development, our setup provides an exceptional developer experience. Developers can:</p>
<ol>
<li><strong>Explore APIs</strong> using Scalar&#39;s beautiful interface, testing endpoints directly from the documentation</li>
<li><strong>Monitor request flows</strong> using local OpenTelemetry exporters to understand how their changes affect the broader system</li>
<li><strong>Debug issues</strong> using correlated logs and traces that show exactly what happened during a specific request</li>
<li><strong>Ensure compatibility</strong> by testing against versioned APIs that clearly communicate breaking changes</li>
</ol>
<h3 id="production-operations">Production Operations</h3>
<p>In production, our observability stack provides operations teams with the insights they need to maintain system reliability:</p>
<ul>
<li><strong>Azure Monitor integration</strong> provides enterprise-grade alerting, dashboards, and analysis capabilities</li>
<li><strong>Distributed tracing</strong> enables rapid root cause analysis when issues occur across service boundaries</li>
<li><strong>Comprehensive metrics</strong> support proactive monitoring and capacity planning</li>
<li><strong>Structured logging</strong> with trace correlation makes troubleshooting specific customer issues straightforward</li>
</ul>
<h3 id="security-and-compliance">Security and Compliance</h3>
<p>Our <code>ActionControllerLogging</code> filter creates detailed audit trails for compliance requirements, while the audit data flows through Azure Service Bus to ensure reliable delivery and processing. The combination of OpenTelemetry traces and custom audit logs provides both technical observability and business-level audit capabilities.</p>
<h2 id="performance-and-overhead-considerations">Performance and Overhead Considerations</h2>
<p>Implementing comprehensive observability always raises questions about performance impact. OpenTelemetry is designed with performance in mind, using efficient sampling strategies and asynchronous export mechanisms to minimize impact on application performance.</p>
<p>Our instrumentation focuses on high-value, low-overhead data collection. ASP.NET Core instrumentation adds minimal latency to request processing, while HTTP client instrumentation helps us identify slow downstream dependencies. Runtime instrumentation provides valuable insights into garbage collection and memory usage with negligible overhead.</p>
<p>The key is selective instrumentation—we capture the telemetry that provides actionable insights while avoiding over-instrumentation that could impact performance or create overwhelming amounts of data.</p>
<h2 id="future-enhancements-and-roadmap">Future Enhancements and Roadmap</h2>
<p>Looking ahead, our observability and documentation strategy continues to evolve. Some areas we&#39;re exploring include:</p>
<p><strong>Enhanced Custom Metrics</strong>: Adding business-specific metrics that help us understand not just technical performance but business outcomes.</p>
<p><strong>Improved Documentation</strong>: Exploring ways to include more context in our API documentation, such as usage examples and integration guides.</p>
<p><strong>Advanced Tracing</strong>: Investigating custom span creation for critical business processes to provide even more detailed insights into application behavior.</p>
<p><strong>Multi-Environment Correlation</strong>: Developing strategies to correlate telemetry across development, staging, and production environments to better understand how code changes affect system behavior.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The integration of OpenTelemetry, Scalar, and OpenAPI in our .NET 9 solution creates a powerful foundation for building and operating modern microservices. OpenTelemetry provides vendor-neutral observability that scales with our application, Scalar delivers beautiful, developer-friendly API documentation, and OpenAPI ensures our service contracts are clear, versioned, and automatically maintained.</p>
<p>This combination supports both development velocity and operational excellence. Developers get the tools they need to understand and test APIs, while operations teams get the observability data required to maintain reliable, performant systems.</p>
<p>Most importantly, this approach is future-proof. By embracing open standards like OpenTelemetry and OpenAPI, we avoid vendor lock-in while still benefiting from excellent tooling and integrations. As our architecture evolves and new requirements emerge, our observability and documentation strategy can adapt without requiring fundamental changes.</p>
<p>The investment in proper observability and documentation pays dividends throughout the application lifecycle—from initial development through production operations to long-term maintenance and evolution. In today&#39;s fast-paced development environment, these capabilities aren&#39;t just helpful; they&#39;re essential for building systems that teams can confidently develop, deploy, and operate at scale.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Github Copilot in the real world</title>
            <link>https://www.architect4hire.com/blog/github-copilot-in-the-real-world</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/github-copilot-in-the-real-world</guid>
            <pubDate>Thu, 03 Jul 2025 14:58:22 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>AI-Assisted Development</category>
            <description>Copilot demos beautifully and disappoints quietly. The difference between the two is everything you do around it — how you prompt it, where you trust it, and where you absolutely do not.</description>
            <content:encoded><![CDATA[<p>In today&#39;s rapidly evolving tech landscape, developers are constantly seeking tools and approaches to accelerate development cycles while maintaining code quality. GitHub Copilot has emerged as a game-changer, transforming how developers approach cloud-based development, particularly when working with ASP.NET Core 9 and microservices architectures. This comprehensive guide explores how to leverage GitHub Copilot effectively in modern cloud development scenarios, highlighting best practices and potential pitfalls.</p>
<h2 id="understanding-github-copilot-in-2025">Understanding GitHub Copilot in 2025</h2>
<p>GitHub Copilot has evolved significantly since its initial release, becoming an indispensable AI pair programmer that understands context, suggests complete functions, and helps developers navigate complex codebases. In 2025, Copilot offers a suite of powerful features specifically optimized for cloud-native development workflows.</p>
<h3 id="key-features-enhancing-net-development">Key Features Enhancing .NET Development</h3>
<p>GitHub Copilot now offers specialized capabilities for .NET developers, including:</p>
<ol>
<li><strong>Intelligent Code Completion</strong>: Beyond simple autocompletion, Copilot now understands ASP.NET Core 9&#39;s architecture and can suggest entire controller methods, middleware configurations, and service registrations based on your project context.</li>
<li><strong>Copilot in CLI</strong>: Get real-time assistance directly in your terminal for command suggestions and explanations when working with the dotnet CLI, Docker commands, or Kubernetes configurations.</li>
<li><strong>Application Modernization</strong>: The dedicated Visual Studio extension helps upgrade legacy .NET projects to newer versions, update dependencies, and apply code fixes automatically, making migration to ASP.NET Core 9 significantly smoother.</li>
<li><strong>.NET Aspire Integration</strong>: With the release of .NET Aspire 9.3, GitHub Copilot provides specialized assistance for cloud-native application development, including orchestrated container deployments and service discovery configurations.</li>
<li><strong>Real-time Documentation</strong>: Copilot can generate and explain documentation for complex microservice architectures, saving valuable development time while ensuring knowledge transfer across teams.</li>
</ol>
<h2 id="asp-net-core-9-cloud-ready-foundation-for-microservices">ASP.NET Core 9: Cloud-Ready Foundation for Microservices</h2>
<p>ASP.NET Core 9 brings substantial improvements specifically designed for cloud-native and microservices development:</p>
<h3 id="cloud-native-features">Cloud-Native Features</h3>
<ol>
<li><strong>Enhanced Performance</strong>: ASP.NET Core 9 offers improved request processing throughput, making it ideal for high-load microservice scenarios.</li>
<li><strong>Built-in Security Enhancements</strong>: Advanced authorization policies, improved data protection APIs, and better integration with cloud identity providers streamline secure microservices deployment.</li>
<li><strong>Minimal APIs Advancement</strong>: Further refinements to the minimal API approach make creating lightweight microservices even more straightforward and maintainable.</li>
<li><strong>Cloud Integration</strong>: Native integration with major cloud providers simplifies deployment and management of containerized applications.</li>
<li><strong>Cross-Platform Excellence</strong>: True platform independence ensures consistent behavior across Windows, Linux, and cloud environments.</li>
</ol>
<h2 id="leveraging-github-copilot-for-microservices-architecture">Leveraging GitHub Copilot for Microservices Architecture</h2>
<p>Microservices architecture continues to be the preferred approach for building scalable, maintainable cloud applications. Here&#39;s how GitHub Copilot can be leveraged effectively in this context:</p>
<h3 id="do-use-copilot-for-service-interface-design">DO: Use Copilot for Service Interface Design</h3>
<p>When designing service interfaces, Copilot excels at suggesting consistent API patterns based on your existing codebase:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Simply start defining your controller and let Copilot suggest the rest</span>
[<span class="hljs-meta">ApiController</span>]
[<span class="hljs-meta">Route(<span class="hljs-string">&quot;api/[controller]&quot;</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderController</span> : <span class="hljs-title">ControllerBase</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IOrderService _orderService;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">OrderController</span>(<span class="hljs-params">IOrderService orderService</span>)</span>
    {
        _orderService = orderService;
    }

    <span class="hljs-comment">// Copilot will suggest CRUD operations with appropriate status codes,</span>
    <span class="hljs-comment">// validation, error handling, and documentation</span>
}</code></pre>
<p>Copilot will understand your domain model and suggest appropriate endpoints, status codes, and error handling approaches consistent with your existing services.</p>
<h3 id="don-and-39-t-rely-on-copilot-for-critical-business-logic-without-review">DON&#39;T: Rely on Copilot for Critical Business Logic Without Review</h3>
<p>While Copilot is excellent at generating boilerplate code and common patterns, critical business logic should always be reviewed carefully:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DON&#x27;T blindly accept complex business logic suggestions</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;<span class="hljs-built_in">decimal</span>&gt; <span class="hljs-title">CalculateOrderTotal</span>(<span class="hljs-params">OrderRequest request</span>)</span>
{
    <span class="hljs-comment">// Copilot might suggest a calculation that looks reasonable</span>
    <span class="hljs-comment">// but misses your specific business rules or edge cases</span>
    <span class="hljs-comment">// ALWAYS review these implementations carefully!</span>
}</code></pre>
<p>Always review business logic suggestions to ensure they align with your specific requirements, especially for financial calculations, security-critical operations, or complex domain rules.</p>
<h3 id="do-use-copilot-for-configuration-and-middleware-setup">DO: Use Copilot for Configuration and Middleware Setup</h3>
<p>ASP.NET Core 9&#39;s middleware pipeline and service configuration can be complex. Copilot excels at suggesting appropriate middleware ordering and configuration:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Start typing your Program.cs setup and Copilot will help</span>
<span class="hljs-keyword">var</span> builder = WebApplication.CreateBuilder(<span class="hljs-keyword">args</span>);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

<span class="hljs-comment">// Copilot can suggest appropriate service registrations</span>
<span class="hljs-comment">// for your microservices, including health checks, monitoring,</span>
<span class="hljs-comment">// and distributed tracing</span>
builder.Services.AddHealthChecks()
    .AddCheck&lt;DatabaseHealthCheck&gt;(<span class="hljs-string">&quot;database&quot;</span>)
    .AddCheck&lt;ExternalApiHealthCheck&gt;(<span class="hljs-string">&quot;external-api&quot;</span>);

<span class="hljs-comment">// Copilot will suggest appropriate middleware ordering</span>
<span class="hljs-keyword">var</span> app = builder.Build();

<span class="hljs-keyword">if</span> (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks(<span class="hljs-string">&quot;/health&quot;</span>);</code></pre>
<h3 id="don-and-39-t-generate-complex-container-orchestration-without-understanding">DON&#39;T: Generate Complex Container Orchestration Without Understanding</h3>
<p>Container orchestration with Docker Compose or Kubernetes manifests is an area where Copilot can help, but blindly accepting suggestions can lead to problems:</p>
<pre class="blog-code"><code class="hljs language-yaml"><span class="hljs-comment"># Copilot can generate Docker Compose files, but ensure you understand</span>
<span class="hljs-comment"># the networking, volume mounts, and resource allocations</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">api:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">yourregistry/api:latest</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;8080:80&quot;</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ConnectionStrings__DefaultConnection=Server=db;Database=master;User=sa;Password=${DB_PASSWORD}</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">db</span>
  <span class="hljs-comment"># Review configurations thoroughly before deployment</span></code></pre>
<p>Always understand the infrastructure implications of generated configurations, especially regarding security, networking, and resource allocation.</p>
<h2 id="best-practices-for-microservices-development-with-copilot">Best Practices for Microservices Development with Copilot</h2>
<h3 id="1-service-boundary-definition">1. Service Boundary Definition</h3>
<p>Use Copilot to help identify and maintain clear service boundaries:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Let Copilot help with defining clear service contracts</span>
<span class="hljs-keyword">namespace</span> <span class="hljs-title">OrderService.Contracts</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IOrderService</span>
    {
        <span class="hljs-comment">// Copilot can suggest a well-defined service interface</span>
        <span class="hljs-comment">// with appropriate DTOs and operation contracts</span>
    }
}</code></pre>
<p>Copilot can analyze your domain model and suggest appropriate service boundaries, but always ensure these align with your business domains and team structures.</p>
<h3 id="2-communication-patterns">2. Communication Patterns</h3>
<p>GitHub Copilot excels at implementing various communication patterns required for microservices:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Use Copilot to implement communication patterns</span>
<span class="hljs-comment">// Synchronous HTTP communication</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderClient</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> HttpClient _httpClient;

    <span class="hljs-comment">// Copilot can generate resilient HTTP client implementations</span>
    <span class="hljs-comment">// with appropriate retry policies, circuit breakers, etc.</span>
}

<span class="hljs-comment">// Message-based communication</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderCreatedEventHandler</span> : <span class="hljs-title">IEventHandler</span>&lt;<span class="hljs-title">OrderCreatedEvent</span>&gt;
{
    <span class="hljs-comment">// Copilot can suggest appropriate message handling patterns</span>
    <span class="hljs-comment">// based on your chosen messaging infrastructure</span>
}</code></pre>
<h3 id="3-data-consistency-strategies">3. Data Consistency Strategies</h3>
<p>Microservices often require sophisticated data consistency strategies. Copilot can help implement patterns like:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// Saga pattern implementation</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderProcessingSaga</span> : <span class="hljs-title">ISaga</span>&lt;<span class="hljs-title">OrderProcessingState</span>&gt;
{
    <span class="hljs-comment">// Copilot can suggest appropriate saga steps, compensation actions,</span>
    <span class="hljs-comment">// and state management approaches</span>
}

<span class="hljs-comment">// Outbox pattern for reliable message publishing</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderRepository</span> : <span class="hljs-title">IOrderRepository</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">CreateOrder</span>(<span class="hljs-params">Order order</span>)</span>
    {
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> transaction = <span class="hljs-keyword">await</span> _dbContext.Database.BeginTransactionAsync();
        <span class="hljs-comment">// Copilot can help implement the transactional outbox pattern</span>
        <span class="hljs-comment">// to ensure reliable message publishing with database transactions</span>
    }
}</code></pre>
<p>While Copilot provides good starting points, always review these implementations carefully as they&#39;re critical to system reliability.</p>
<h2 id="cloud-deployment-and-devops-integration">Cloud Deployment and DevOps Integration</h2>
<p>GitHub Copilot shines in streamlining cloud deployments and DevOps practices for ASP.NET Core 9 microservices:</p>
<h3 id="do-generate-infrastructure-as-code-templates">DO: Generate Infrastructure as Code Templates</h3>
<p>Copilot can help create comprehensive infrastructure definitions:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Use Copilot to generate Pulumi/Terraform/ARM templates</span>
<span class="hljs-comment">// Example Pulumi C# code for deploying a microservice to Azure</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">InfrastructureStack</span> : <span class="hljs-title">Stack</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">InfrastructureStack</span>()</span>
    {
        <span class="hljs-keyword">var</span> resourceGroup = <span class="hljs-keyword">new</span> ResourceGroup(<span class="hljs-string">&quot;myResourceGroup&quot;</span>, <span class="hljs-keyword">new</span> ResourceGroupArgs
        {
            Location = <span class="hljs-string">&quot;EastUS&quot;</span>
        });

        <span class="hljs-comment">// Copilot can generate appropriate infrastructure definitions</span>
        <span class="hljs-comment">// for your specific cloud provider and requirements</span>
    }
}</code></pre>
<h3 id="don-and-39-t-ignore-security-best-practices-in-generated-configurations">DON&#39;T: Ignore Security Best Practices in Generated Configurations</h3>
<p>Copilot might generate code that works but doesn&#39;t follow security best practices:</p>
<pre class="blog-code"><code class="hljs language-yaml"><span class="hljs-comment"># DON&#x27;T: Accept security-sensitive configurations without review</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Secret</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">database-credentials</span>
<span class="hljs-attr">data:</span>
  <span class="hljs-comment"># Copilot might suggest hardcoded credentials or insecure practices</span>
  <span class="hljs-comment"># ALWAYS review security-sensitive configurations</span>
  <span class="hljs-attr">username:</span> <span class="hljs-string">YWRtaW4=</span>  <span class="hljs-comment"># base64 encoded &quot;admin&quot;</span>
  <span class="hljs-attr">password:</span> <span class="hljs-string">cGFzc3dvcmQxMjM=</span>  <span class="hljs-comment"># base64 encoded &quot;password123&quot;</span></code></pre>
<p>Always ensure generated infrastructure code follows security best practices, especially regarding secrets management, network security, and access controls.</p>
<h2 id="testing-microservices-with-copilot">Testing Microservices with Copilot</h2>
<p>GitHub Copilot can significantly accelerate test creation:</p>
<h3 id="do-use-copilot-for-test-case-generation">DO: Use Copilot for Test Case Generation</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Let Copilot suggest test cases based on your implementation</span>
[<span class="hljs-meta">Fact</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">GetOrder_WithValidId_ReturnsOrder</span>()</span>
{
    <span class="hljs-comment">// Arrange</span>
    <span class="hljs-keyword">var</span> orderId = Guid.NewGuid();
    <span class="hljs-keyword">var</span> expectedOrder = <span class="hljs-keyword">new</span> Order { Id = orderId, Customer = <span class="hljs-string">&quot;Test Customer&quot;</span> };
    _orderRepositoryMock.Setup(repo =&gt; repo.GetOrderByIdAsync(orderId))
        .ReturnsAsync(expectedOrder);

    <span class="hljs-comment">// Act</span>
    <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> _controller.GetOrder(orderId);

    <span class="hljs-comment">// Assert</span>
    <span class="hljs-keyword">var</span> okResult = Assert.IsType&lt;OkObjectResult&gt;(result.Result);
    <span class="hljs-keyword">var</span> returnedOrder = Assert.IsType&lt;OrderDto&gt;(okResult.Value);
    Assert.Equal(orderId, returnedOrder.Id);
    Assert.Equal(<span class="hljs-string">&quot;Test Customer&quot;</span>, returnedOrder.Customer);
}</code></pre>
<p>Copilot can analyze your implementation and suggest comprehensive test cases covering various scenarios, including edge cases and error conditions.</p>
<h3 id="don-and-39-t-skip-integration-testing-of-microservice-communication">DON&#39;T: Skip Integration Testing of Microservice Communication</h3>
<p>While unit tests are important, microservices require thorough integration testing:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Use Copilot to help write integration tests</span>
[<span class="hljs-meta">Fact</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">OrderAPI_Integration_CreatesOrderAndNotifiesShipping</span>()</span>
{
    <span class="hljs-comment">// Arrange</span>
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> application = <span class="hljs-keyword">new</span> WebApplicationFactory&lt;Program&gt;();
    <span class="hljs-keyword">var</span> client = application.CreateClient();

    <span class="hljs-comment">// Act &amp; Assert</span>
    <span class="hljs-comment">// Copilot can help generate comprehensive integration test scenarios</span>
    <span class="hljs-comment">// that verify cross-service communication works correctly</span>
}</code></pre>
<p>Copilot can help generate test fixtures and assertions, but ensure your tests cover the critical integration points between services.</p>
<h2 id="performance-optimization-and-monitoring">Performance Optimization and Monitoring</h2>
<p>ASP.NET Core 9 offers advanced performance features that can be leveraged with Copilot:</p>
<h3 id="do-use-copilot-for-implementing-performance-monitoring">DO: Use Copilot for Implementing Performance Monitoring</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Let Copilot help implement monitoring and telemetry</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ConfigureServices</span>(<span class="hljs-params">IServiceCollection services</span>)</span>
{
    <span class="hljs-comment">// Copilot can suggest appropriate monitoring configurations</span>
    services.AddOpenTelemetry()
        .WithTracing(builder =&gt; builder
            .AddSource(<span class="hljs-string">&quot;OrderService&quot;</span>)
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddSqlClientInstrumentation()
            .AddOtlpExporter())
        .WithMetrics(builder =&gt; builder
            .AddMeter(<span class="hljs-string">&quot;Microsoft.AspNetCore.Hosting&quot;</span>)
            .AddMeter(<span class="hljs-string">&quot;Microsoft.AspNetCore.Server.Kestrel&quot;</span>)
            .AddOtlpExporter());
}</code></pre>
<h3 id="don-and-39-t-over-optimize-prematurely">DON&#39;T: Over-Optimize Prematurely</h3>
<p>Copilot might suggest aggressive optimization techniques that aren&#39;t necessary:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DON&#x27;T: Implement complex optimizations without measuring first</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IEnumerable&lt;OrderSummary&gt;&gt; GetRecentOrders()
{
    <span class="hljs-comment">// Copilot might suggest complex caching strategies or query optimizations</span>
    <span class="hljs-comment">// that add unnecessary complexity before you&#x27;ve identified a performance issue</span>

    <span class="hljs-comment">// Start with simple, readable implementations and optimize based on actual metrics</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> _orderRepository.GetRecentOrdersAsync();
}</code></pre>
<p>Always measure performance before applying complex optimizations, even when Copilot suggests them.</p>
<h2 id="error-handling-and-resilience">Error Handling and Resilience</h2>
<p>Modern cloud applications need robust error handling and resilience patterns:</p>
<h3 id="do-use-copilot-for-implementing-resilience-patterns">DO: Use Copilot for Implementing Resilience Patterns</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Let Copilot help implement resilience patterns</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> : <span class="hljs-title">IOrderService</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IHttpClientFactory _clientFactory;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;OrderResult&gt; <span class="hljs-title">CreateOrder</span>(<span class="hljs-params">OrderRequest request</span>)</span>
    {
        <span class="hljs-comment">// Copilot can suggest appropriate resilience patterns</span>
        <span class="hljs-keyword">var</span> client = _clientFactory.CreateClient(<span class="hljs-string">&quot;shipping-service&quot;</span>);

        <span class="hljs-keyword">var</span> policy = Policy
            .Handle&lt;HttpRequestException&gt;()
            .OrResult&lt;HttpResponseMessage&gt;(r =&gt; !r.IsSuccessStatusCode)
            .WaitAndRetryAsync(<span class="hljs-number">3</span>, retryAttempt =&gt; TimeSpan.FromSeconds(Math.Pow(<span class="hljs-number">2</span>, retryAttempt)));

        <span class="hljs-keyword">var</span> response = <span class="hljs-keyword">await</span> policy.ExecuteAsync(() =&gt; client.PostAsJsonAsync(<span class="hljs-string">&quot;/api/shipments&quot;</span>, request));
        <span class="hljs-comment">// Handle response appropriately</span>
    }
}</code></pre>
<h3 id="don-and-39-t-ignore-proper-error-propagation">DON&#39;T: Ignore Proper Error Propagation</h3>
<p>Copilot might not always suggest the most appropriate error handling strategy:</p>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DON&#x27;T: Accept simplistic error handling suggestions</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">ProcessPayment</span>(<span class="hljs-params">PaymentRequest request</span>)</span>
{
    <span class="hljs-keyword">try</span>
    {
        <span class="hljs-comment">// Process payment</span>
        <span class="hljs-keyword">return</span> Ok();
    }
    <span class="hljs-keyword">catch</span> (Exception ex)
    {
        <span class="hljs-comment">// Copilot might suggest overly broad exception handling</span>
        <span class="hljs-comment">// that masks important errors or doesn&#x27;t provide enough context</span>
        <span class="hljs-keyword">return</span> StatusCode(<span class="hljs-number">500</span>, <span class="hljs-string">&quot;An error occurred&quot;</span>);
    }
}

<span class="hljs-comment">// DO: Implement proper error handling with appropriate details and logging</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">ProcessPayment</span>(<span class="hljs-params">PaymentRequest request</span>)</span>
{
    <span class="hljs-keyword">try</span>
    {
        <span class="hljs-comment">// Process payment</span>
        <span class="hljs-keyword">return</span> Ok();
    }
    <span class="hljs-keyword">catch</span> (PaymentProviderException ex)
    {
        _logger.LogWarning(ex, <span class="hljs-string">&quot;Payment provider error for order {OrderId}&quot;</span>, request.OrderId);
        <span class="hljs-keyword">return</span> StatusCode(<span class="hljs-number">503</span>, <span class="hljs-keyword">new</span> ProblemDetails
        {
            Title = <span class="hljs-string">&quot;Payment service temporarily unavailable&quot;</span>,
            Detail = <span class="hljs-string">&quot;The payment couldn&#x27;t be processed due to a temporary issue with the payment provider&quot;</span>,
            Status = <span class="hljs-number">503</span>
        });
    }
    <span class="hljs-keyword">catch</span> (Exception ex)
    {
        _logger.LogError(ex, <span class="hljs-string">&quot;Unexpected error processing payment for order {OrderId}&quot;</span>, request.OrderId);
        <span class="hljs-keyword">return</span> StatusCode(<span class="hljs-number">500</span>, <span class="hljs-keyword">new</span> ProblemDetails
        {
            Title = <span class="hljs-string">&quot;Internal server error&quot;</span>,
            Detail = <span class="hljs-string">&quot;An unexpected error occurred processing your payment&quot;</span>,
            Status = <span class="hljs-number">500</span>
        });
    }
}</code></pre>
<h2 id="security-considerations">Security Considerations</h2>
<p>Security is paramount in cloud-based microservices. GitHub Copilot can both help and hinder:</p>
<h3 id="do-use-copilot-for-security-best-practices-implementation">DO: Use Copilot for Security Best Practices Implementation</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DO: Let Copilot help implement security best practices</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ConfigureServices</span>(<span class="hljs-params">IServiceCollection services</span>)</span>
{
    <span class="hljs-comment">// Copilot can suggest appropriate security configurations</span>
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =&gt;
        {
            options.Authority = Configuration[<span class="hljs-string">&quot;Authentication:Authority&quot;</span>];
            options.Audience = Configuration[<span class="hljs-string">&quot;Authentication:Audience&quot;</span>];
            options.TokenValidationParameters = <span class="hljs-keyword">new</span> TokenValidationParameters
            {
                ValidateIssuer = <span class="hljs-literal">true</span>,
                ValidateAudience = <span class="hljs-literal">true</span>,
                ValidateLifetime = <span class="hljs-literal">true</span>,
                ValidateIssuerSigningKey = <span class="hljs-literal">true</span>
            };
        });

    services.AddAuthorization(options =&gt;
    {
        options.AddPolicy(<span class="hljs-string">&quot;OrdersRead&quot;</span>, policy =&gt;
            policy.RequireClaim(<span class="hljs-string">&quot;scope&quot;</span>, <span class="hljs-string">&quot;orders:read&quot;</span>));
        options.AddPolicy(<span class="hljs-string">&quot;OrdersWrite&quot;</span>, policy =&gt;
            policy.RequireClaim(<span class="hljs-string">&quot;scope&quot;</span>, <span class="hljs-string">&quot;orders:write&quot;</span>));
    });
}</code></pre>
<h3 id="don-and-39-t-accept-generated-security-critical-code-without-review">DON&#39;T: Accept Generated Security-Critical Code Without Review</h3>
<pre class="blog-code"><code class="hljs language-csharp"><span class="hljs-comment">// DON&#x27;T: Blindly accept security-critical code</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PasswordHasher</span> : <span class="hljs-title">IPasswordHasher</span>
{
    <span class="hljs-comment">// Copilot might suggest insecure practices or outdated algorithms</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">HashPassword</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> password</span>)</span>
    {
        <span class="hljs-comment">// NEVER accept security-critical code without careful review</span>
        <span class="hljs-comment">// and alignment with current best practices</span>
    }
}</code></pre>
<p>Always have security-critical code reviewed, regardless of whether it was generated by Copilot.</p>
<h2 id="conclusion">Conclusion</h2>
<p>GitHub Copilot represents a transformative tool for modern cloud development with ASP.NET Core 9 and microservices. When used judiciously, it can dramatically accelerate development while maintaining code quality. The key to success lies in understanding where Copilot excels (boilerplate, patterns, configuration) and where human expertise remains essential (security, business logic, architecture).</p>
<p>By following the best practices outlined in this guide, developers can harness the power of AI-assisted coding while avoiding common pitfalls. As cloud-native development continues to evolve, the partnership between human developers and tools like GitHub Copilot will become increasingly symbiotic, enabling teams to deliver sophisticated microservice architectures with greater efficiency and quality than ever before.</p>
<p>The future of cloud development is collaborative—between humans and AI—and GitHub Copilot is at the forefront of this revolution, particularly for ASP.NET Core 9 and modern microservices architectures. Embrace this powerful tool, but do so thoughtfully, and you&#39;ll unlock new levels of productivity in your cloud development journey.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Domain-Driven Design with Onion Architecture in .NET 9: Building Scalable Microservices</title>
            <link>https://www.architect4hire.com/blog/domain-driven-design-with-onion-architecture-in-net-9-building-scalable-microservices</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/domain-driven-design-with-onion-architecture-in-net-9-building-scalable-microservices</guid>
            <pubDate>Thu, 03 Jul 2025 14:08:39 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Architecture &amp; Patterns</category>
            <description>Architecture still matters, and most systems prove it the hard way. A real-world implementation of Domain-Driven Design on Onion Architecture in .NET 9 — dependencies pointing inward, business reality in the code.</description>
            <content:encoded><![CDATA[<p>In modern software development, creating systems that are maintainable, testable, and adaptable to change is crucial. Our platform showcases a real-world implementation of Domain-Driven Design (DDD) combined with Onion Architecture principles, all built on .NET 9. This architectural approach has allowed us to develop a robust microservices ecosystem that delivers both business agility and technical excellence.</p>
<p>.NET has evolved dramatically over the past decade. With .NET 9 and ASP.NET Core 9, Microsoft’s flagship platform has doubled down on performance, cloud-native microservices, and developer productivity. But as the tools get more powerful, the architectures we choose to build with them matter more than ever.</p>
<p>If you’re a developer or architect trying to keep your codebases clean, maintainable, and adaptable in a world of microservices, buzzwords like <strong>Domain-Driven Design (DDD)</strong>, <strong>Onion Architecture</strong>, and <strong>Clean Architecture</strong> are probably familiar. But how do these ideas actually fit together? How do you put them into practice in a real ASP.NET Core 9 solution? And how do you ensure your system stays robust as it scales?</p>
<p>In this post, we’ll break all that down. We’ll look at how Clean Architecture, Onion Architecture, and Domain-Driven Design complement each other — and how you can implement these principles in an ASP.NET Core 9 microservices solution that’s easy to reason about, test, and evolve.</p>
<p>By the end, you’ll have a practical roadmap to help you structure your next enterprise-ready .NET 9 application.</p>
<h2 id="why-architecture-still-matters"><strong>Why Architecture Still Matters</strong></h2>
<p>The .NET 9 ecosystem is rich with features: minimal APIs, powerful performance optimizations, improved cloud-native tooling, and first-class support for containerized deployments and orchestration.</p>
<p>But even the best framework can’t save a poorly structured application.</p>
<p>Modern business requirements change fast. You might need to pivot your domain logic, adapt to new integrations, split services apart, or rewrite pieces without taking the whole system down.</p>
<p>If your code is tangled up — if it’s just “controller spaghetti” directly tied to your database models — you’ll hit walls fast. That’s where proper architecture comes in.</p>
<p><em>Clean Architecture</em> and <em>Onion Architecture</em> are design patterns that help you organize your solution in a way that:</p>
<ul>
<li>Puts your <strong>business logic at the center</strong>, protected from external concerns.</li>
<li>Makes dependencies flow in one direction — inward.</li>
<li>Keeps infrastructure details (like databases, web APIs, message queues) on the outer layers, easy to swap or replace.</li>
<li>Encourages strong boundaries and separation of concerns.</li>
</ul>
<p>When you pair this with <strong>Domain-Driven Design</strong>, you also ensure that your code reflects the real business domain — the real problems you’re solving — instead of just being a CRUD wrapper around tables.</p>
<h2 id="the-microservices-landscape">The Microservices Landscape</h2>
<p>The platform consists of multiple interdependent yet decoupled microservices, each responsible for a specific business domain:</p>
<pre class="blog-code"><code class="hljs language-plaintext">// From AppHost/Program.cs - Our microservices orchestration
var token = builder.AddProject&lt;Projects.Networks_Token&gt;(&quot;tokenservice&quot;);

var buddy = builder.AddProject&lt;Projects.Networks_BuddyService&gt;(&quot;buddyservice&quot;)
    .WithReference(cache).WaitFor(cache)
    .WithReference(token).WaitFor(token)
    .WithReference(seq).WaitFor(seq);

var profile = builder.AddProject&lt;Projects.Networks_ProfileService&gt;(&quot;profileservice&quot;)
    .WithReference(cache).WaitFor(cache)
    .WithReference(token).WaitFor(token)
    .WithReference(seq).WaitFor(seq);

// Additional services...</code></pre>
<p>This structure allows us to scale, deploy, and maintain each service independently. Let&#39;s explore how DDD and Onion Architecture principles guide the design of each service.</p>
<h2 id="domain-driven-design-aligning-code-with-business-reality">Domain-Driven Design: Aligning Code with Business Reality</h2>
<p>Domain-Driven Design puts the focus on the core domain and domain logic, mapping business concepts to code structures. In the solution, each microservice represents a bounded context - a logical boundary around a specific domain model.</p>
<h3 id="ubiquitous-language">Ubiquitous Language</h3>
<p>A cornerstone of DDD is establishing a common language shared between developers and domain experts. In our codebase, this is evident in how our domain models reflect business concepts:</p>
<pre class="blog-code"><code class="hljs language-plaintext">// Domain models that reflect our business language
public class ProfileDomainModel
{
    public Guid Id { get; set; }
    public Guid Guid { get; set; }
    public string UserName { get; set; }
    public string NormalizedUserName { get; set; }
    public string Email { get; set; }
    public Site Site { get; set; }
    public string Location { get; set; }
    public int? LocationId { get; set; }
    public bool IsOnline { get; set; }
    public DateTime? LastOnline { get; set; }
    // Additional properties...
}</code></pre>
<p>This domain model captures the essence of a user profile in our system, using terms that business stakeholders understand.</p>
<h3 id="bounded-contexts">Bounded Contexts</h3>
<p>Each microservice maintains its own bounded context. For example, the ProfileService manages everything related to user profiles, while the BuddyService handles relationships between users. These services communicate through well-defined interfaces, preserving context boundaries.</p>
<h2 id="onion-architecture-dependencies-point-inward">Onion Architecture: Dependencies Point Inward</h2>
<p>Onion Architecture (also known as Clean Architecture or Hexagonal Architecture) organizes code in concentric layers, with domain models at the center. Dependencies always point inward, ensuring that the core business logic remains independent of infrastructure concerns.</p>
<h3 id="the-domain-layer-at-the-core">The Domain Layer: At the Core</h3>
<p>The domain layer represents the business concepts and rules:</p>
<pre class="blog-code"><code class="hljs language-plaintext">// Domain interfaces define the contract without implementation details
public interface IIntegrationData
{
    Task&lt;ProfileDomainModel&gt; FetchProfileAsync(Guid userGuid, Site site);
    Task&lt;List&lt;ProfileDomainModel&gt;&gt; FetchProfilListAsync(List&lt;Guid&gt; userGuids, Site site);
    Task&lt;List&lt;HoneyPotDomainModel&gt;&gt; GetHoneyPotAsync(Guid userGuid, Site site);
    Task MarkUserAsLoggedOffAsync(Guid userGuid, Site site);
    Task&lt;ProfileDomainModel&gt; UpsertDefaultProfileAsync(string userName, string email, Site site);
}</code></pre>
<p>These interfaces define what the system needs from a business perspective, without specifying how these operations are implemented.</p>
<h3 id="the-data-layer-infrastructure-concerns">The Data Layer: Infrastructure Concerns</h3>
<p>The data layer implements the domain interfaces, connecting to databases and other external systems:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public class IntegrationData : IIntegrationData
{
    private readonly ProfileDbContext _context;

    public IntegrationData(ProfileDbContext context)
    {
        _context = context;
    }

    public Task&lt;ProfileDomainModel&gt; FetchProfileAsync(Guid userGuid, Site site)
    {
        return _context.Profiles
            .Where(up =&gt; up.Guid == userGuid &amp;&amp; up.Site == site)
            .FirstOrDefaultAsync();
    }

    // Other implementation methods...
}</code></pre>
<p>Here, the data layer depends on domain interfaces but implements them using Entity Framework Core. The domain layer remains completely unaware of these implementation details.</p>
<h3 id="the-business-layer-application-services">The Business Layer: Application Services</h3>
<p>The business layer orchestrates operations and enforces business rules:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public class IntegrationBusiness : IIntegrationBusiness
{
    private readonly IIntegrationData _data;

    public IntegrationBusiness(IIntegrationData data)
    {
        _data = data;
    }

    public async Task&lt;ProfileServiceModel&gt; FetchProfileAsync(Guid userGuid, Site site)
    {
        var domain = await _data.FetchProfileAsync(userGuid, site);
        if (domain == null)
        {
            throw new ProfileNotFoundException($&quot;Profile not found for userGuid: {userGuid} on site: {site}&quot;);
        }
        return domain.MapToServiceModel();
    }

    // Other business methods...
}</code></pre>
<p>The business layer depends on abstractions from both the domain and data layers, but never on concrete implementations. This ensures flexibility and testability.</p>
<h2 id="the-facade-pattern-simplifying-interactions">The Facade Pattern: Simplifying Interactions</h2>
<p>One key architectural choice in solution is the extensive use of the Facade pattern. The Facade sits between the business layer and the API layer, providing a simplified interface for clients and handling cross-cutting concerns:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public class IntegrationFacade : IIntegrationFacade
{
    private readonly IIntegrationBusiness _business;
    private readonly CacheAside _cache;

    public IntegrationFacade(IIntegrationBusiness business, IDistributedCache cache)
    {
        _business = business;
        _cache = new CacheAside(cache);
    }

    public async Task&lt;Result&lt;ProfileServiceModel&gt;&gt; FetchProfileAsync(Guid userGuid, Site site)
    {
        try
        {
            if (userGuid == Guid.Empty || !Enum.IsDefined(typeof(Site), site))
                return Result&lt;ProfileServiceModel&gt;.Failure(Error.InvalidInput, 
                    new List&lt;string&gt; { &quot;Invalid userGuid or site.&quot; });

            string cacheKey = $&quot;profile_{userGuid}_{site}&quot;;
            var data = await _cache.FetchFromCache&lt;ProfileServiceModel&gt;(cacheKey, async () =&gt;
            {
                return await _business.FetchProfileAsync(userGuid, site);
            }, new DistributedCacheEntryOptions() { AbsoluteExpiration = DateTime.UtcNow.AddHours(24) });

            return Result&lt;ProfileServiceModel&gt;.Success(data);
        }
        catch (ProfileNotFoundException)
        {
            return Result&lt;ProfileServiceModel&gt;.Failure(Error.NotFound, 
                new List&lt;string&gt; { &quot;Profile not found.&quot; });
        }
    }

    // Other facade methods...
}</code></pre>
<p>The Facade pattern offers several benefits:</p>
<ol>
<li><strong>Centralized Input Validation</strong>: The Facade layer validates inputs before passing them to the business layer.</li>
<li><strong>Standardized Error Handling</strong>: Exceptions are caught and converted into consistent response formats.</li>
<li><strong>Cross-Cutting Concerns</strong>: Caching, logging, and other aspects are managed here.</li>
<li><strong>Simplified API</strong>: Controllers interact with a clean, consistent interface.</li>
</ol>
<h2 id="dependency-injection-wiring-it-all-together">Dependency Injection: Wiring It All Together</h2>
<p>.NET 9&#39;s built-in dependency injection container manages the instantiation and lifetime of our components:</p>
<pre class="blog-code"><code class="hljs language-plaintext">void AddScopes(WebApplicationBuilder builder)
{
    builder.Services.AddScoped&lt;IIntegrationFacade, IntegrationFacade&gt;();
    builder.Services.AddScoped&lt;IIntegrationBusiness, IntegrationBusiness&gt;();
    builder.Services.AddScoped&lt;IIntegrationData, IntegrationData&gt;();
    builder.Services.AddScoped&lt;IProfileFacade, ProfileFacade&gt;();
    builder.Services.AddScoped&lt;IProfileBusiness, ProfileBusiness&gt;();
    builder.Services.AddScoped&lt;IProfileData, ProfileData&gt;();
    builder.Services.AddScoped&lt;IHoneyPotFacade, HoneyPotFacade&gt;();
    builder.Services.AddScoped&lt;IHoneyPotBusiness, HoneyPotBusiness&gt;();
    builder.Services.AddScoped&lt;IHoneyPotData, HoneyPotData&gt;();
    builder.Services.AddScoped&lt;IServiceBusSender, ServiceBusSender&gt;();
    // Additional registrations...
}</code></pre>
<p>This configuration allows for flexible composition while maintaining the dependency inversion principle. Higher-level components depend on abstractions, not concrete implementations.</p>
<h2 id="api-layer-thin-controllers">API Layer: Thin Controllers</h2>
<p>With our layered architecture in place, controllers remain remarkably thin:</p>
<pre class="blog-code"><code class="hljs language-plaintext">[ApiController]
[Route(&quot;api/buddy/v{v:apiVersion}/[controller]&quot;)]
[ApiVersion(1)]
public class IntegrationController : ControllerBase
{
    private readonly IIntegrationFacade _facade;

    public IntegrationController(IIntegrationFacade facade)
    {
        _facade = facade;
    }

    [HttpGet(&quot;{userGuid:guid}/{site:int}&quot;)]
    public async Task&lt;IActionResult&gt; GetBuddies(Guid userGuid, int site)
    {
        var result = await _facade.GetBuddies(userGuid, (Site)site);
        return result.Match();
    }

    // Other endpoint methods...
}</code></pre>
<p>The controllers simply map HTTP requests to facade calls and convert the results to HTTP responses, without any business logic.</p>
<h2 id="inter-service-communication">Inter-Service Communication</h2>
<p>In a microservices architecture, services need to communicate. We implement two primary methods:</p>
<h3 id="http-clients-with-refit">HTTP Clients with Refit</h3>
<p>For synchronous communication, we use typed HTTP clients with Refit:</p>
<pre class="blog-code"><code class="hljs language-plaintext">void AddHttpClients(WebApplicationBuilder builder)
{
    builder.Services.AddRefitClient&lt;IProfileHttpClient&gt;()
        .ConfigureHttpClient(c =&gt; c.BaseAddress = new Uri(
            builder.Environment.EnvironmentName != &quot;LocalDevelopment&quot; 
                ? builder.Configuration[&quot;profileurl&quot;] 
                : &quot;https+http://profileservice&quot;))
        .AddHttpMessageHandler(() =&gt; new AuthHeaderHandler(builder.Services.BuildServiceProvider()));

    // Additional HTTP clients...
}</code></pre>
<p>These strongly-typed clients ensure type safety when communicating between services.</p>
<h3 id="service-bus-for-asynchronous-communication">Service Bus for Asynchronous Communication</h3>
<p>For event-based communication, we leverage Azure Service Bus:</p>
<pre class="blog-code"><code class="hljs language-plaintext">builder.Services.AddSingleton&lt;ServiceBusClient&gt;(sp =&gt;
    new ServiceBusClient(builder.Configuration.GetConnectionString(&quot;servicebus&quot;)));</code></pre>
<p>This facilitates loosely coupled, asynchronous interactions between services.</p>
<h2 id="infrastructure-as-code-with-net-aspire">Infrastructure as Code with .NET Aspire</h2>
<p>Our solution leverages .NET Aspire to define and orchestrate the entire microservices ecosystem:</p>
<pre class="blog-code"><code class="hljs language-plaintext">var builder = DistributedApplication.CreateBuilder(args);

// Infrastructure components
var cache = builder.AddRedis(&quot;cache&quot;)
    .WithLifetime(ContainerLifetime.Persistent)
    .WithHostPort(6379)
    .WithRedisInsight()
    .WithRedisCommander();

// Service definitions with dependencies
var profile = builder.AddProject&lt;Projects.Networks_ProfileService&gt;(&quot;profileservice&quot;)
    .WithReference(cache).WaitFor(cache)
    .WithReference(token).WaitFor(token)
    .WithReference(seq).WaitFor(seq);</code></pre>
<p>This declarative approach ensures consistent deployment and proper dependency management.</p>
<h2 id="cross-cutting-concerns">Cross-Cutting Concerns</h2>
<p>Several aspects span multiple services and layers:</p>
<h3 id="caching">Caching</h3>
<p>The Facade layer implements a cache-aside pattern to improve performance:</p>
<pre class="blog-code"><code class="hljs language-plaintext">var data = await _cache.FetchFromCache&lt;BuddyListServiceModel&gt;(cacheKey, async () =&gt;
{
    return await _business.GetBuddies(userGuid, site);
}, new DistributedCacheEntryOptions() { AbsoluteExpiration = DateTime.UtcNow.AddHours(24) });</code></pre>
<h3 id="authentication-and-authorization">Authentication and Authorization</h3>
<p>Security is managed consistently across services:</p>
<pre class="blog-code"><code class="hljs language-plaintext">void AddAuthentication(WebApplicationBuilder builder)
{
    builder.Services.AddAuthentication(options =&gt;
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =&gt;
    {
        var key = Encoding.UTF8.GetBytes(
            builder.Configuration[&quot;Jwt:Key&quot;];
            
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration[&quot;Jwt:Issuer&quot;],
            ValidateAudience = true,
            ValidAudience = builder.Configuration[&quot;Jwt:Audience&quot;],
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });
}</code></pre>
<h3 id="logging-and-monitoring">Logging and Monitoring</h3>
<p>OpenTelemetry integration provides comprehensive observability:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public static TBuilder ConfigureOpenTelemetry&lt;TBuilder&gt;(this TBuilder builder) 
    where TBuilder : IHostApplicationBuilder
{
    builder.Logging.AddOpenTelemetry(logging =&gt;
    {
        logging.IncludeFormattedMessage = true;
        logging.IncludeScopes = true;
    });

    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics =&gt;
        {
            metrics.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation();
        })
        .WithTracing(tracing =&gt;
        {
            tracing.AddSource(builder.Environment.ApplicationName)
                .AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation();
        });

    // Configure exporters...
    return builder;
}</code></pre>
<h2 id="benefits-of-our-architectural-approach">Benefits of Our Architectural Approach</h2>
<p>This DDD + Onion Architecture approach delivers numerous advantages:</p>
<h3 id="1-testability">1. Testability</h3>
<p>Since business logic depends only on abstractions, unit testing becomes straightforward:</p>
<pre class="blog-code"><code class="hljs language-plaintext">[TestMethod]
public async Task AssociateUserAsync_Valid_CallsDataAndProfile()
{
    // Arrange
    var model = new AssociateViewModel { UserName = userName, Level = level };
    _userDataMock.Setup(d =&gt; d.AssociateUserAsync(userName, level, site))
        .ReturnsAsync(email);
    _profileMock.Setup(p =&gt; p.UpsertDefaultProfile(userName, email, (int)site))
        .ReturnsAsync(Result&lt;ProfileServiceModel&gt;.Success(
            new ProfileServiceModel { Guid = Guid.NewGuid() }));

    // Act
    await _business.AssociateUserAsync(model, site);

    // Assert
    _userDataMock.Verify(d =&gt; d.AssociateUserAsync(userName, level, site), Times.Once);
    _profileMock.Verify(p =&gt; p.UpsertDefaultProfile(userName, email, (int)site), Times.Once);
}</code></pre>
<h3 id="2-maintainability">2. Maintainability</h3>
<p>The clear separation of concerns makes the codebase more maintainable. Developers can understand and modify specific parts of the system without needing to comprehend the entire system.</p>
<h3 id="3-flexibility">3. Flexibility</h3>
<p>Implementation details can change without affecting the core business logic:</p>
<ul>
<li>Switching from Cosmos DB to SQL Server would only affect the Data layer</li>
<li>Changing from REST APIs to gRPC would only impact the API layer</li>
<li>Updating caching strategies would be isolated to the Facade layer</li>
</ul>
<h3 id="4-scalability">4. Scalability</h3>
<p>Each microservice can scale independently based on its specific load characteristics, optimizing resource usage.</p>
<h2 id="challenges-and-solutions">Challenges and Solutions</h2>
<p>While the benefits are significant, this architecture isn&#39;t without challenges:</p>
<h3 id="1-development-complexity">1. Development Complexity</h3>
<p><strong>Challenge</strong>: More layers and abstractions mean more code to write initially.</p>
<p><strong>Solution</strong>: We established clear templates and conventions to streamline development and maintain consistency.</p>
<h3 id="2-distributed-data-management">2. Distributed Data Management</h3>
<p><strong>Challenge</strong>: With each service owning its data, ensuring consistency across services is complex.</p>
<p><strong>Solution</strong>: We employ event-driven patterns for eventual consistency and carefully design service boundaries to minimize cross-service transactions.</p>
<h3 id="3-performance-overhead">3. Performance Overhead</h3>
<p><strong>Challenge</strong>: Additional layers and service boundaries can introduce performance overhead.</p>
<p><strong>Solution</strong>: Strategic caching in the Facade layer, connection pooling, and efficient serialization mitigate these concerns.</p>
<h2 id="conclusion-enabling-business-agility-through-technical-excellence">Conclusion: Enabling Business Agility Through Technical Excellence</h2>
<p>Our DDD + Onion Architecture approach has enabled our client to grow and evolve rapidly while maintaining code quality and developer productivity. By ensuring that our code structure reflects business realities and maintains clean separation of concerns, we&#39;ve created a system that&#39;s both technically sound and business-aligned.</p>
<p>The combination of Domain-Driven Design, Onion Architecture, and the Facade pattern has proven particularly powerful. It allows domain experts to contribute meaningfully to the design, helps developers understand the business context, and maintains clean boundaries between different aspects of the system.</p>
<p>As the platform continues to grow, this architectural foundation will enable us to adapt to changing requirements, scale individual components as needed, and incorporate new technologies without destabilizing the entire system. The result is a microservices ecosystem that delivers business value consistently while remaining technically maintainable and evolvable.</p>
<p>In an era where software must continuously adapt to changing business needs, investing in solid architectural principles isn&#39;t just a technical concern—it&#39;s a strategic business advantage. Our experience with this project demonstrates that Domain-Driven Design, Onion Architecture, and clean separation of concerns provide a robust foundation for building complex, scalable, and maintainable microservices systems in .NET 9.</p>
]]></content:encoded>
        </item>
        <item>
            <title>How the Architect4Hire SOA Framework Powers Real .NET Aspire Applications in the Wild</title>
            <link>https://www.architect4hire.com/blog/how-the-architect4hire-soa-framework-powers-real-net-aspire-applications-in-the-wild</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/how-the-architect4hire-soa-framework-powers-real-net-aspire-applications-in-the-wild</guid>
            <pubDate>Sat, 14 Jun 2025 11:11:07 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>Architecture &amp; Patterns</category>
            <description>Cutting through the marketing gloss on .NET Aspire: how it is actually used in production, by real teams, for real clients, on an architecture that scales.</description>
            <content:encoded><![CDATA[<p>If you’ve been paying attention to .NET’s evolution into a full-fledged cloud-native powerhouse, you’ve likely heard about <a href="https://learn.microsoft.com/en-us/dotnet/aspire/" target="_blank" rel="noopener noreferrer">.NET Aspire</a>. But let’s cut through the marketing gloss and talk about how it&#39;s actually used in production by real teams, for real clients with a solid architecture that scales.</p>
<p>At Architect4Hire, we’ve been pushing the limits of what ASP.NET Core 9 and .NET Aspire can do, and our latest framework implementation for BuddyNetworks is a showcase of modern software architecture done right.</p>
<p>From microservice orchestration to clean separation of concerns, it’s the kind of stuff that makes devs sleep better at night and apps hum in production.</p>
<hr>
<h2 id="building-blocks-multi-layer-architecture-the-real-way">🧱 Building Blocks: Multi-Layer Architecture, the Real Way</h2>
<p>At the heart of the Architect4Hire framework is a well-structured multi-layer architecture that enforces a strict separation of concerns. Each layer is responsible for a focused set of responsibilities. The Presentation Layer handles HTTP requests, routing, and authentication. The Domain Layer is subdivided into facade, business, and data sublayers—each encapsulating orchestration, core logic, and persistence, respectively. The Shared Layer houses reusable utilities and enums, and implements a <code>Result</code> pattern to standardize error handling. This clear architectural design improves maintainability, enhances testability, and keeps teams productive even as the codebase scales. This structure is the foundation for building reliable and adaptable systems.</p>
<p>Example controller:</p>
<pre class="blog-code"><code class="hljs language-plaintext">[Authorize]
[ApiController]
[Route(&quot;api/v{v:apiVersion}/[controller]&quot;)]
[ApiVersion(1)]
public class UserController : ControllerBase
{
    private readonly IUserFacade _userFacade;

    public UserController(IUserFacade userFacade)
    {
        _userFacade = userFacade;
    }

    [HttpPost(&quot;register&quot;)]
    public async Task&lt;IActionResult&gt; RegisterUser([FromBody] RegisterViewModel model, [FromHeader] int site)
    {
        var result = await _userFacade.RegisterUserAsync(model, site);
        return result.Match();
    }
}</code></pre>
<h2 id="soa-aspire-clean-scalable-microservices">🔁 SOA + Aspire = Clean, Scalable Microservices</h2>
<p>The framework is built around a service-oriented architecture (SOA) where each domain is represented by its own microservice. These services, such as <code>UserService</code>, <code>AdminService</code>, and <code>ProfileService</code>, communicate via REST and Azure Service Bus. The .NET Aspire project plays a critical role in service discovery, observability, and orchestration. Aspire allows each service to be discovered dynamically and monitored with health checks and distributed tracing. It makes the deployment and scaling of services in cloud-native environments seamless and secure. By leveraging Aspire, we&#39;re able to provide a modern, production-grade microservice ecosystem with very little friction and excellent visibility into system health.</p>
<p>AppHost and service discovery:</p>
<pre class="blog-code"><code class="hljs language-plaintext">builder.Services.AddRefitClient&lt;IAdminHttpClient&gt;()
    .ConfigureHttpClient(c =&gt; c.BaseAddress = new Uri(
        builder.Environment.IsProduction()
            ? builder.Configuration[&quot;adminurl&quot;]
            : &quot;https+http://adminservice&quot;));

builder.AddAzureServiceBusClient(&quot;servicebus&quot;);</code></pre>
<p><img src="/img/blog/how-the-architect4hire-soa-framework-powers-real-net-aspire-applications-in-the-wild/img-1.png" alt="How the Architect4Hire SOA Framework Powers Real .NET Aspire Applications in the Wild" loading="lazy" decoding="async"></p>
<h2 id="aspire-empowers-local-development-too">🧪 Aspire Empowers Local Development Too</h2>
<p>One of the lesser-sung advantages of .NET Aspire is how much it improves the local development experience. Aspire supports virtualized cloud resources, allowing developers to simulate complex distributed systems without requiring full Azure infrastructure locally. Developers can run all microservices in tandem, inspect traffic between them, and leverage local emulators for Azure services, such as Service Bus or Cosmos DB. The Aspire AppHost automatically wires up service dependencies, enabling consistent startup across environments. This means devs spend less time fiddling with Docker or local secrets and more time writing and debugging actual features. It’s local-first cloud-native development, done right.</p>
<p>Example Aspire app configuration:</p>
<pre class="blog-code"><code class="hljs language-plaintext">var builder = DistributedApplication.CreateBuilder(args);

var cosmos = builder.AddAzureCosmosDB(&quot;cosmos&quot;)
    .WithDatabase(&quot;BuddyNetworks&quot;);

var userService = builder.AddProject&lt;Projects.UserService&gt;(&quot;userservice&quot;)
    .WithReference(cosmos);

builder.Build().Run();</code></pre>
<p><img src="/img/blog/how-the-architect4hire-soa-framework-powers-real-net-aspire-applications-in-the-wild/img-2.png" alt="How the Architect4Hire SOA Framework Powers Real .NET Aspire Applications in the Wild" loading="lazy" decoding="async"></p>
<h2 id="asp-net-core-9-in-action">✨ ASP.NET Core 9 in Action</h2>
<p>ASP.NET Core 9 provides a modern, high-performance foundation for building cloud-native APIs. With support for minimal APIs, developers can write faster, leaner endpoints that are easier to test and maintain. Versioned routing, detailed response annotations, and OpenAPI integration make documentation and client development straightforward. In the Architect4Hire framework, we implement consistent response types using <code>Result.Match()</code>, which converts domain results into HTTP responses cleanly. We also integrate FluentValidation for input validation and use claims-based JWT authentication to secure endpoints. ASP.NET Core 9 gives us the flexibility, performance, and security features needed to meet enterprise-grade software requirements.</p>
<p>Using <code>Match()</code> for clean responses:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public static IActionResult Match&lt;TModel&gt;(this Result&lt;TModel&gt; result) where TModel : ServiceBaseModel
{
    if (result.IsSuccess)
    {
        return new OkObjectResult(result);
    }

    return result.Error switch
    {
        Error.NotFound =&gt; new NotFoundObjectResult(result),
        Error.InvalidInput =&gt; new BadRequestObjectResult(result),
        Error.Unauthorized =&gt; new UnauthorizedObjectResult(result),
        Error.Forbidden =&gt; new ForbidResult(),
        Error.InternalServerError =&gt; new ObjectResult(result) { StatusCode = 500 },
        Error.DuplicateEmail =&gt; new ObjectResult(result) { StatusCode = 432 },
        _ =&gt; new ObjectResult(result) { StatusCode = 500 }
    };
}</code></pre>
<h2 id="the-facade-pattern-our-secret-weapon">🧐 The Facade Pattern: Our Secret Weapon</h2>
<p>The Facade Layer is one of the standout features of the Architect4Hire framework. It acts as an intermediary between the HTTP-facing Presentation Layer and the logic-rich Business Layer. This pattern abstracts away validation, exception translation, and cross-service orchestration, keeping the controllers thin and focused. Facades validate input using FluentValidation, convert domain exceptions into typed errors, and return standardized <code>Result</code> objects to maintain consistency across the API. The pattern ensures that changing business rules doesn&#39;t require controller refactoring, making the application more resilient to change. It also enhances testability and reusability by isolating orchestration from routing logic.</p>
<p>Example facade method:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public async Task&lt;Result&lt;NoResponseServiceModel&gt;&gt; RegisterUserAsync(RegisterViewModel model, int site)
{
    var validationResult = _registerValidator.Validate(model);
    if (!validationResult.IsValid)
    {
        var errorMessages = validationResult.Errors.Select(e =&gt; e.ErrorMessage).ToList();
        return Result&lt;NoResponseServiceModel&gt;.Failure(Error.InvalidInput, errorMessages);
    }

    try
    {
        await _userBusiness.RegisterUserAsync(model, site);
        return Result&lt;NoResponseServiceModel&gt;.Success();
    }
    catch (DuplicateEmailException)
    {
        return Result&lt;NoResponseServiceModel&gt;.Failure(Error.DuplicateEmail, new List&lt;string&gt;());
    }
    catch (Exception ex)
    {
        return Result&lt;NoResponseServiceModel&gt;.Failure(Error.InternalServerError, new List&lt;string&gt; { ex.Message });
    }
}</code></pre>
<h2 id="data-done-right">📃 Data Done Right</h2>
<p>Data persistence in the Architect4Hire SOA Framework is handled through clean repository-style interfaces and EF Core-backed implementations. SQL Server is used for relational storage, especially for identity and transactional data, while Cosmos DB supports high-throughput document storage for services like profiles. The <code>UserData</code> class encapsulates all Identity-related operations, throwing custom domain exceptions for known failure conditions (e.g., duplicate username or email). This allows business logic to handle failures in a domain-aware fashion. Every data layer implementation is unit-testable, and our framework encourages clear boundaries, preventing business logic from leaking into persistence. It’s a clean, robust approach to modern data management.</p>
<p>Sample data operation:</p>
<pre class="blog-code"><code class="hljs language-plaintext">public async Task&lt;string&gt; RegisterUserAsync(string email, string userName, string password, MemberLevel level, int site)
{
    var user = new ApplicationUser { Email = email, UserName = userName, EmailConfirmed = false };
    var result = await _userManager.CreateAsync(user, password);

    if (!result.Succeeded)
    {
        switch (result.Errors.FirstOrDefault()?.Code.ToLower())
        {
            case &quot;duplicateusername&quot;: throw new DuplicateUserNameException(&quot;Username already exists.&quot;);
            case &quot;duplicateemail&quot;: throw new DuplicateEmailException(&quot;Email already exists.&quot;);
            default: throw new Exception(&quot;User registration failed: &quot; + string.Join(&quot;, &quot;, result.Errors.Select(e =&gt; e.Description)));
        }
    }

    await _userManager.AddClaimAsync(user, new Claim(&quot;MemberBB4N&quot;, &quot;true&quot;));
    await _userManager.AddClaimAsync(user, new Claim(&quot;BB4NLevel&quot;, level.ToString()));

    return user.Id;
}</code></pre>
<p><img src="/img/blog/how-the-architect4hire-soa-framework-powers-real-net-aspire-applications-in-the-wild/img-3.png" alt="How the Architect4Hire SOA Framework Powers Real .NET Aspire Applications in the Wild" loading="lazy" decoding="async"></p>
<h2 id="testing-because-production-is-not-qa">🧪 Testing, Because Production is Not QA</h2>
<p>Robust testing practices are baked into every layer of our framework. We employ unit tests for business logic and validation, integration tests for verifying component interactions, and full-stack API tests for endpoint coverage. For example, <code>RegisterUserAsync</code> is tested from input validation all the way through to Cosmos DB writes and email orchestration. We use in-memory test doubles and mocking to simulate external services. Testing is integrated into our CI/CD pipeline using GitHub Actions and Azure DevOps, ensuring that no code reaches production without proper coverage. Testing isn&#39;t an afterthought—it&#39;s foundational to delivering stable, predictable software in real-world deployments.</p>
<p>Unit test strategy example:</p>
<pre class="blog-code"><code class="hljs language-plaintext">[Fact]
public async Task RegisterUserAsync_ShouldReturnSuccess_WhenValidInput()
{
    var model = new RegisterViewModel { UserName = &quot;testuser&quot;, Email = &quot;test@example.com&quot;, Password = &quot;Password123&quot;, Level = MemberLevel.Standard };
    var result = await _facade.RegisterUserAsync(model, site: 1);

    Assert.True(result.IsSuccess);
}</code></pre>
<h2 id="want-a-template-like-this">Want a Template Like This?</h2>
<p>This architecture isn’t locked away behind corporate doors. The Architect4Hire SOA Framework is available as a reusable solution template that you can adopt or adapt to your own business needs. Whether you&#39;re building a dating app, a logistics platform, or a social media service, this architecture provides a head start with proven patterns, a working service mesh, and all the boilerplate wiring done correctly. Want a copy? Reach out to us directly.</p>
<p>📍 <strong>Visit</strong><a href="https://architect4hire.com/" target="_blank" rel="noopener noreferrer">Architect4Hire.com</a> and let’s talk about how we can help you build better software, faster.</p>
]]></content:encoded>
        </item>
        <item>
            <title>Aspire Cafe - A hands-on template for Aspire and ASP.NET Core</title>
            <link>https://www.architect4hire.com/blog/aspire-cafe-a-hands-on-template-for-aspire-and-asp-net-core</link>
            <guid isPermaLink="true">https://www.architect4hire.com/blog/aspire-cafe-a-hands-on-template-for-aspire-and-asp-net-core</guid>
            <pubDate>Sun, 11 May 2025 14:49:56 GMT</pubDate>
            <dc:creator>Robert Felkins</dc:creator>
            <category>.NET &amp; Azure</category>
            <description>Building cloud-native apps can feel like juggling flaming swords while riding a unicycle. Aspire Café is a hands-on, working template — microservices, orchestration, infrastructure and patterns you can actually read.</description>
            <content:encoded><![CDATA[<p>Let’s face it—building modern cloud-native apps can feel like juggling flaming swords while riding a unicycle. That’s where <a href="https://architect4hire.com/" target="_blank" rel="noopener noreferrer">architect4hire.com</a> comes in. Whether knee-deep in code or mapping out the next big system architecture, Architect4Hire offers guidance, patterns, and real-world examples to help you get there faster (with fewer headaches).</p>
<p>Our Aspire Café project, built with .NET Aspire and ASP.NET Core, is like a hands-on blueprint for developers who want to learn by doing. It takes all those best practices we hear about—like clean architecture, microservices, and containerized deployments—and shows them in action. So if you want to level up your cloud-native game, Architect4Hire’s got your back.</p>
<p>Make sure to bookmark the git repo at <a href="https://github.com/architect4hire/aspirecafe" target="_blank" rel="noopener noreferrer">https://github.com/architect4hire/aspirecafe</a></p>
<h2 id="microservices-the-caf-and-39-s-backbone">☕ Microservices: The Café&#39;s Backbone</h2>
<p>The Aspire Café solution exemplifies a microservices architecture, where each service is dedicated to a specific business function:</p>
<ul>
<li><strong>Product API</strong>: Manages the product catalog and inventory.</li>
<li><strong>Counter API</strong>: Handles order processing and payments.</li>
<li><strong>Kitchen API</strong>: Manages food preparation workflows.</li>
<li><strong>Barista API</strong>: Handles beverage preparation workflows.</li>
<li><strong>Order Summary API</strong>: Aggregates order information across services.</li>
</ul>
<p>Each service adheres to the Single Responsibility Principle, allowing for independent development, deployment, and scaling. This modularity enhances fault isolation and facilitates targeted updates.</p>
<hr>
<h2 id="net-aspire-application-host-orchestrating-the-services">🔧 .NET Aspire Application Host: Orchestrating the Services</h2>
<p>At the heart of the solution is the <strong>Aspire Application Host</strong>, which orchestrates the various services and infrastructure components. It simplifies the management of distributed applications by handling:</p>
<ul>
<li><strong>Service registration and dependencies</strong>: Automatically wires up services and their dependencies.</li>
<li><strong>Infrastructure provisioning</strong>: Manages resources like databases, caches, and message brokers.</li>
<li><strong>Service discovery and configuration</strong>: Facilitates service communication through automatic discovery and configuration.</li>
</ul>
<p>This orchestration enhances the local development experience by simplifying the management of the application&#39;s configuration and interconnections.</p>
<hr>
<h2 id="infrastructure-components-building-blocks-of-the-caf">🏗️ Infrastructure Components: Building Blocks of the Café</h2>
<p>The solution leverages several cloud-native services, managed through Aspire:</p>
<ul>
<li><strong>Azure Cosmos DB</strong>: A NoSQL database for storing product and order information. The .NET Aspire integration enables seamless connection to existing Cosmos DB instances or creation of new ones.</li>
<li><strong>Redis Cache</strong>: Provides distributed caching for improved performance. Aspire&#39;s integration allows for easy setup and management of Redis instances.</li>
<li><strong>Azure Service Bus</strong>: Acts as a message broker for asynchronous communication between services. Aspire simplifies the integration with Azure Service Bus.</li>
<li><strong>Azure Key Vault</strong>: Securely stores credentials and secrets. The solution uses an emulator for development purposes.</li>
</ul>
<p>Each component is containerized for local development, ensuring consistency across environments.</p>
<hr>
<h2 id="design-patterns-and-practices-structuring-the-codebase">🧱 Design Patterns and Practices: Structuring the Codebase</h2>
<p>The solution employs several design patterns to enhance maintainability and testability:</p>
<h3 id="layered-onion-style-architecture">Layered (Onion Style) Architecture</h3>
<p>Each microservice implements a clean layered architecture:</p>
<ul>
<li><strong>API Layer</strong>: Controllers handling HTTP requests.</li>
<li><strong>Facade Layer</strong>: Orchestrates business logic and validation.</li>
<li><strong>Business Layer</strong>: Implements core business rules.</li>
<li><strong>Data Layer</strong>: Manages data access and persistence.</li>
</ul>
<p>This separation of concerns improves maintainability and testability.</p>
<h3 id="facade-pattern">Facade Pattern</h3>
<p>The facade pattern provides a simplified interface to complex subsystems, decoupling the client (controller) from the implementation details of the business logic. This pattern enhances code readability and maintainability.</p>
<h3 id="result-pattern">Result Pattern</h3>
<p>A custom <code>Result</code> type is used to handle operation outcomes consistently, avoiding exception-based flow control and providing clear error information.</p>
<h3 id="validation-pattern">Validation Pattern</h3>
<p>FluentValidation is employed for model validation, separating validation rules from the models. This ensures robust input validation before business operations.</p>
<hr>
<h2 id="api-documentation-clear-and-comprehensive">📚 API Documentation: Clear and Comprehensive</h2>
<p>The controllers utilize XML documentation and <code>ProducesResponseType</code> attributes to generate comprehensive API documentation. This approach integrates well with OpenAPI and tools like Scalar.net, providing developers with explicit and interactive API references.</p>
<hr>
<h2 id="modern-c-and-net-features-leveraging-the-latest">🚀 Modern C# and .NET Features: Leveraging the Latest</h2>
<p>The solution takes advantage of the latest features in C# 13 and .NET 9:</p>
<ul>
<li><strong>Async/await patterns</strong>: Facilitate non-blocking operations.</li>
<li><strong>Pattern matching</strong>: Enhances code clarity and conciseness.</li>
<li><strong>Extension methods</strong>: Promote cleaner code organization.</li>
<li><strong>Middleware</strong>: Handles cross-cutting concerns like exception handling.</li>
</ul>
<p>These features contribute to a modern, efficient, and maintainable codebase.</p>
<hr>
<h2 id="simply-put-a-robust-foundation-for-cloud-native-applications">☁️ Simply Put A Robust Foundation for Cloud-Native Applications</h2>
<p>The Aspire Café solution demonstrates how modern .NET applications can be built using a microservices architecture with the .NET Aspire stack. The application achieves scalability and maintainability by leveraging clean design patterns, cloud-native infrastructure, and the latest language features.</p>
<p>This architecture provides a solid foundation for building complex, distributed systems that can evolve independently while working together seamlessly.</p>
]]></content:encoded>
        </item>
    </channel>
</rss>
