<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Luke Parker (Hona)]]></title><description><![CDATA[Luke is a Senior Software Engineer, an expert in .NET and Vertical Slice Architecture. With a passion for sharing knowledge, he loves educating the developer co]]></description><link>https://lukeparker.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1737530340016/d7c72851-8f79-4e7e-a463-0e88b7520f9a.png</url><title>Luke Parker (Hona)</title><link>https://lukeparker.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 07 Apr 2026 19:43:01 GMT</lastBuildDate><atom:link href="https://lukeparker.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Be a good citizen using AI in open-source and the workplace]]></title><description><![CDATA[I am frustrated, I think a lot of people are frustrated.
This is a memo I want to share as I’ve written these thoughts in several messages and calls and pub chats.
People are ruining companies and open source.
This applies to any collaborative enviro...]]></description><link>https://lukeparker.dev/be-a-good-citizen-using-ai-in-open-source-and-the-workplace</link><guid isPermaLink="true">https://lukeparker.dev/be-a-good-citizen-using-ai-in-open-source-and-the-workplace</guid><category><![CDATA[AI]]></category><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 08 Jan 2026 01:16:37 GMT</pubDate><content:encoded><![CDATA[<p>I am frustrated, I think a lot of people are frustrated.</p>
<p>This is a memo I want to share as I’ve written these thoughts in several messages and calls and pub chats.</p>
<p>People are ruining companies and open source.</p>
<p>This applies to any collaborative environment, I do not care how you use AI on your own personal hobby project. Perhaps it applies if you have users in production.</p>
<p>All my points apply to any work you do. This could be a PR, a config change, a new 3rd party tool, a word document, an email. All work may or may not have AI assistance these days.</p>
<h2 id="heading-implicit-trust">Implicit trust</h2>
<p>My primary point is that when you do work there is an unspoken agreement that you understand what you did. You also understand why you did it. What you did works, compiles, runs, works when doing basic exploration.</p>
<p>When you use AI to help you work - if you do not understand what it is doing, why, or have not FULLY done your due dilligence you:</p>
<ul>
<li><p>Insult whoever you show the work (PR reviewer, email reader, customer using your app)</p>
<ul>
<li>You expect THEM to do the due dillegence on behalf of you - you break things, they report it, then you vibe another solution without doing the due dillegence. you get the point.</li>
</ul>
</li>
<li><p>Will not learn the product or grow as an asset to the company/project.</p>
<ul>
<li><p>In the age of AI one of the benefits is that you should have more focus on product. Learn and understand the code, architecture AND business. There is no continual improvements, or human ideas that are the next moonshot for the company if you’re not at a baseline understanding what work the AI is doing for you.</p>
</li>
<li><p>You also are expected to learn from you mistakes - an outage is okay, but understand the root cause, and fix the culture that allowed it to happen in the first place. A vibe coded solution or feature means you will NEVER learn and are doomed to making the same mistake over and over again. As the complexity increases over time with new features, you have exponentially more outages, issues, loss of quality.</p>
</li>
</ul>
</li>
</ul>
<p>We are all guilty of taking shortcuts.</p>
<p>‘Oh this just changes the text on the page, I don’t need to compile’</p>
<p>Maybe this change was inside a YAML file and you run into <a target="_blank" href="https://www.bram.us/2022/01/11/yaml-the-norway-problem/">the norway problem</a>. What I mean is there are side effects of things you can never account for, no amount of ‘that is probably fine’ will get you by.</p>
<p>In the age before the AI boom, good citizens would do this 1% of the time.</p>
<p>I am now seeing the opposite - this is 99% of the time, and I am really sick of it.</p>
<h2 id="heading-the-illusion-of-expertise">The illusion of expertise</h2>
<p>As models get better and better, they write code that looks well structured, seasoned and pragmatic. You see a PR and it looks great - merge it, then the most basic things break.</p>
<p>Before AI coding, you could get a rough guage of skill from looking at submitted code. Now it goes out the window.</p>
<p>We are moving to an engineering culture that favours personal reputation over code. Its really hard to guage if a perfect sounding patch is thought out, tested, had its due dilligence or not.</p>
<p>You might pull a collegue aside and ask them how to define an array in their primary language they get paid <em>well</em> to write - and yet there is no answer.</p>
<p>Do better. Do not submit code you do not fundamentally understand.</p>
<p>If you DO NOT understand code that agents have generated for you, you might see it as magic. You might think the model is therefore smarter than you, and as such its a perfect change. DO NOT DO THIS AT ALL COSTS.</p>
<h2 id="heading-no-excuse-for-being-spoon-fed">No excuse for being spoon fed</h2>
<p>I see it everywhere online, work chats - all of it.</p>
<p>People ask other people questions, without trying ANYTHING yet.</p>
<p>Open Source maintainers, online ‘helpful’ people or experts are NOT your unpaid personal staff engineers.</p>
<p>Did you even TRY to ask an agent your question?</p>
<p>Did you even TRY to play with the technique you’re asking about?</p>
<p>I’m not saying you shouldn’t ask for help, or be active in communitys - again, everything is shifting to reputation rather than code speaking for itself.</p>
<p>My advise is to exhaust ALL options before asking - and demonstrate what you’ve tried.</p>
<p>Are you submitting an LLM generated message to a human? Did you read the message before sending - or are you insulting the receiving party?</p>
<h2 id="heading-closing-loose-ends">Closing loose ends</h2>
<p>Finishing things is everything. Do not let AI get you 95% of the way there and move on to the new shiny object.</p>
<p>Please follow through and get it DONE. Not mostly done. Done.</p>
<p>You die by a thousand cuts with unfinished work.</p>
<p>Close the loop!</p>
<hr />
<p>I don’t know the solution to these things, we’re seeing volume of slop, and work that is plain lazy, and its not good enough. Do better.</p>
<p>On the upside, for good citizens are able to self service and deep dive with no barriers easier than before.</p>
<p>I personally see this when I am able to fix issues inside of the Bun source code, Open Code project, OS diagnostics, remote servers, Azure configs the list goes on.</p>
<p>Please self reflect - its easy to fall into the traps of modern engineering, which also says that its easier than ever to stand out.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Chatting with AI. Start Loops (Ralph Driven Development)]]></title><description><![CDATA[Typing is a high-friction activity that forces you to filter out "secondary" context. That lost context is usually what causes agent hallucinations.
My stack focuses on high-speed context dumping and stateless execution loops.
The Stack

handy.comput...]]></description><link>https://lukeparker.dev/stop-chatting-with-ai-start-loops-ralph-driven-development</link><guid isPermaLink="true">https://lukeparker.dev/stop-chatting-with-ai-start-loops-ralph-driven-development</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Wed, 31 Dec 2025 00:04:32 GMT</pubDate><content:encoded><![CDATA[<p>Typing is a high-friction activity that forces you to filter out "secondary" context. That lost context is usually what causes agent hallucinations.</p>
<p>My stack focuses on high-speed context dumping and stateless execution loops.</p>
<h3 id="heading-the-stack">The Stack</h3>
<ul>
<li><p><a target="_blank" href="http://handy.computer"><strong>handy.computer</strong></a><strong>:</strong> Offline, local Whisper Turbo. I dictate complex architecture, or even loose conflicting thoughts at speech speed. This removes the barrier to providing the "half-useful" details that actually ground the architecture. Agent planning will question this later.</p>
</li>
<li><p><a target="_blank" href="http://opencode.ai"><strong>opencode.ai</strong></a><strong>:</strong> The terminal agent runner.</p>
</li>
<li><p><strong>CLI &gt; MCP:</strong> I overwhelmingly prefer standard CLI tools over MCP. LLMs are trained on the internet. They know the CLI as model parameters. Agents can run <code>--help</code>. They don't know your custom MCP schema. Save the context window.</p>
<ul>
<li><em>The Exception:</em> <a target="_blank" href="https://github.com/remorses/playwriter">playwriter</a>. It uses standard Playwright syntax and attaches to your <em>real</em> browser to reuse auth/session state.</li>
</ul>
</li>
</ul>
<h3 id="heading-1-ralph-driven-development">1. Ralph Driven Development</h3>
<p>Credit to <a target="_blank" href="https://ghuntley.com/ralph/">Geoffrey Huntley</a> for the methodology.</p>
<p><strong>The Core Philosophy: Erecting Signs</strong> Bad AI results are usually bad prompts, bad context, or bad data access. If the agent fails, do not just fix the code. Fix the prompt. If "Ralph" falls off the slide, you don't just put him back; you put up a sign that says "DON'T JUMP."</p>
<ul>
<li><em>Implementation:</em> I use <a target="_blank" href="http://AGENTS.md"><code>AGENTS.md</code></a> (which OpenCode reads by default) to store these "signs." If the agent figures out a tricky build step, I instruct it to write that knowledge to <a target="_blank" href="http://AGENTS.md"><code>AGENTS.md</code></a> so the next loop doesn't have to rediscover fire.</li>
</ul>
<p><strong>Plans vs. Specs</strong> Huntley advocates for separating requirements into <code>specs/*.md</code> files.</p>
<ul>
<li><em>My take:</em> Currently, I find a massive, well-structured <a target="_blank" href="http://plan.md"><code>plan.md</code></a> sufficient for most tasks, though splitting specs is a valid optimization I am exploring.</li>
</ul>
<p>While I use OpenCode - of course this works with Claude Code or any other harness.</p>
<h3 id="heading-2-the-planning-phase">2. The Planning Phase</h3>
<p>I spend an hour purely on the plan. I do not touch code. I dictate high-level constraints and nitpick API surfaces until I have a <a target="_blank" href="http://plan.md"><code>plan.md</code></a> that often reaches <strong>1,000+ lines</strong>. I read every line, if I am not happy I keep planning.</p>
<p><strong>The Pivot:</strong> Once the plan is solid, change to the build agent and send this prompt to crystallize the state:</p>
<blockquote>
<p>"I love the plan. Please write it to <a target="_blank" href="http://plan.md"><code>plan.md</code></a> in chronological order as a backlog with checkboxes. Each task should be small and isolated. Feel free to create a large backlog so it is specific enough for a new engineer to take over implementation immediately."</p>
</blockquote>
<p>Most engineers would change to Build agent and send ‘go’.</p>
<h3 id="heading-3-the-execution-loop">3. The Execution Loop</h3>
<p>Once <a target="_blank" href="http://plan.md"><code>plan.md</code></a> is frozen, I run a headless loop. This forces the agent to re-read the full context every iteration, eliminating context drift. This ensures the agent has a vague understanding of the past AND the end state.</p>
<p><strong>Bash (macOS/Linux):</strong></p>
<pre><code class="lang-bash"><span class="hljs-keyword">while</span> :; <span class="hljs-keyword">do</span> opencode run -m <span class="hljs-string">"opencode/claude-opus-4-5"</span> <span class="hljs-string">"READ all of plan.md. Pick ONE task. Verify via web/code search. Complete task, verify via CLI/Test output. Commit change. ONLY do one task. Update plan.md. If you learn a critical operational detail (e.g. how to build), update AGENTS.md. If all tasks done, sleep 5s and exit. NEVER GIT PUSH. ONLY COMMIT."</span>; <span class="hljs-keyword">done</span>
</code></pre>
<p><strong>PowerShell (Windows):</strong></p>
<pre><code class="lang-powershell"><span class="hljs-keyword">while</span> (<span class="hljs-variable">$true</span>) { opencode run <span class="hljs-literal">-m</span> <span class="hljs-string">"opencode/claude-opus-4-5"</span> <span class="hljs-string">"READ all of plan.md. Pick ONE task. Verify via web/code search. Complete task, verify via CLI/Test output. Commit change. ONLY do one task. Update plan.md. If you learn a critical operational detail (e.g. how to build), update AGENTS.md. If all tasks done, sleep 5s and exit. NEVER GIT PUSH. ONLY COMMIT."</span> }
</code></pre>
<h3 id="heading-4-verification-amp-backpressure">4. Verification &amp; Backpressure</h3>
<p>The compiler is not just a runner; it is a filter. You need strict backpressure to reject hallucinations before they are committed.</p>
<ul>
<li><p><strong>CLI Verification:</strong> Design codebases to be verifiable via args (e.g., <code>dotnet run -- analyze</code>).</p>
</li>
<li><p><strong>Strictness:</strong> In .NET, I enforce <code>&lt;TreatWarningsAsErrors&gt;true&lt;/TreatWarningsAsErrors&gt;</code> and use the latest Roslyn analyzers in a root <code>Directory.Build.props</code>. If it warns, the loop fails, and Ralph tries again.</p>
</li>
</ul>
<h3 id="heading-5-brownfield-vs-greenfield">5. Brownfield vs. Greenfield</h3>
<p>Huntley argues this is for Greenfield only. <strong>I disagree.</strong></p>
<p>Because I spend significant time planning and never allow the agent to <code>git push</code> (only commit), I maintain a "Human in the Loop" review process. I manually review, apply taste, and test the final state before raising a PR. This makes the technique viable for Brownfield/Legacy codebases, provided you trust your review process more than the agent.</p>
<h3 id="heading-future-roadmap">Future Roadmap</h3>
<p>I am looking into community plugins for OpenCode to integrate a native "Ralph Agent."</p>
<ul>
<li><strong>Parallelism:</strong> Currently, the loop is serial. Future harnesses should identify which tasks in <a target="_blank" href="http://plan.md"><code>plan.md</code></a> can be parallelized (fan-out) and when they must converge for a blocking build step (fan-in).</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[History of Vertical Slice Architecture]]></title><description><![CDATA[History
Architecture is a certainty in software development. There have been many architectures over the years, and you can find a plethora of information on Google.
The history of just Vertical Slice Architecture is a bit more recent. The term was c...]]></description><link>https://lukeparker.dev/history-of-vertical-slice-architecture</link><guid isPermaLink="true">https://lukeparker.dev/history-of-vertical-slice-architecture</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 03 Oct 2024 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-history">History</h1>
<p>Architecture is a certainty in software development. There have been many architectures over the years, and you can find a plethora of information on Google.</p>
<p>The history of just Vertical Slice Architecture is a bit more recent. The term was coined by Jimmy Bogard in 2018. However, the ideas behind it have been around for a while. </p>
<h2 id="heading-2015">2015</h2>
<p>In fact, <a target="_blank" href="https://vimeo.com/131633177">as early as 2015</a>, Jimmy was talking about SOLID Architecture which is essentially the same thing. It had a few key points (from his slides):</p>
<blockquote>
<ul>
<li>SRP - One class per feature/concept</li>
<li>OCP - Extend through cross-cutting concerns</li>
<li>LSP - Just don't do inheritance</li>
<li>ISP - Separating queries from commands</li>
<li>DIP - Save for true external dependencies.</li>
</ul>
</blockquote>
<p>The key points of the SOLID architecture were really applying the principals to achieve a highly cohesive codebase in regard to the Domain specific code (SRP, ISP). The Domain specific code was then extended (OCP) via cross-cutting concerns - like authentication, authorization &amp; generic logging. </p>
<p>A lot of the outcomes from this talk to me, was allowing a simpler architecture for what matters most - the Domain. The most important thing in software, is that it works. The architecture that allows you to deliver features quickly, is the best architecture for you.</p>
<h2 id="heading-2018">2018</h2>
<p>Moving along a few years, in 2018, Jimmy wrote a blog post called <a target="_blank" href="https://www.jimmybogard.com/vertical-slice-architecture/">Vertical Slice Architecture</a>. This was the first time the term was used. It was then shortly followed up with <a target="_blank" href="https://www.youtube.com/watch?v=SUiWfhAhgQw">a presentation at NDC Sydney</a> later that year.</p>
<p>The key points of this evolution of the architecture were (from the presentation):</p>
<ul>
<li>CQRS - e.g. HTTP GET is safe &amp; idempotent, POST is Unsafe &amp; not idempotent. Represent this two different concerns in different models/classes where this was just one.</li>
<li>Vertical Slicing makes it easy to modify code</li>
<li>Do not skip the refactor step in Red-Green-Refactor. (Common complaints about 'simple' architectures is due to this)</li>
<li>Push behaviour down (i.e. Focus on a rich domain &amp; allow handlers to do the 'ugly' work, like data access)</li>
<li>Integration test handlers &amp; unit test Domain</li>
</ul>
<h2 id="heading-where-it-was-left">Where it was left</h2>
<p>At that point, Jimmy has set the world up with some pretty nice foundations.</p>
<ol>
<li>Logically split application reads &amp; writes using CQRS.</li>
<li>CQRS models are use cases. Application use cases should be sliced &amp; tightly coupled within</li>
<li>Refactor repeated logic into a rich Domain</li>
<li>Refactor repeated domain-agnostic code into services, repositories</li>
<li>Integration test use cases</li>
<li>Unit test the Domain</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Why not AutoMapper? What should I use?]]></title><description><![CDATA[Introduction
AutoMapper is one of the most popular mapping libraries in the .NET ecosystem. It is currently sitting at around ~10k stars on GitHub. The library was written by Jimmy Bogard (the VSA guy, the MediatR guy, etc) and has been around for a ...]]></description><link>https://lukeparker.dev/why-not-automapper</link><guid isPermaLink="true">https://lukeparker.dev/why-not-automapper</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 08 Aug 2024 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p><a target="_blank" href="https://github.com/AutoMapper/AutoMapper">AutoMapper</a> is one of the most popular mapping libraries in the .NET ecosystem. It is currently sitting at around ~10k stars on GitHub. The library was written by Jimmy Bogard (the VSA guy, the MediatR guy, etc) and has been around for a long time.</p>
<p>The library is meant to reduce the amount of manual, convention/pattern based mapping code you write in your application. Straight from the README:</p>
<blockquote>
<p>AutoMapper is a simple little library built to solve a deceptively complex problem - getting rid of code that mapped one object to another. This type of code is rather dreary and boring to write, so why not invent a tool to do it for us</p>
</blockquote>
<p>A lot of the pain does come from people using the library in an unintented way - for complex mappings that are not following a convention.</p>
<p>Jimmy writes about this in his blog post: <a target="_blank" href="https://www.jimmybogard.com/dissecting-automapper-progamming-horror/">Dissecting AutoMapper Programming Horror</a></p>
<p>The main takeaway for using AutoMapper is that it should be used for really simple mappings, e.g. for this example:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">WeatherForecast</span>(<span class="hljs-params">DateTime Date, <span class="hljs-keyword">int</span> TemperatureC, <span class="hljs-keyword">string</span> Summary</span>)</span>;
<span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">WeatherForecastDto</span>(<span class="hljs-params">DateTime Date, <span class="hljs-keyword">int</span> TemperatureC, <span class="hljs-keyword">string</span> Summary</span>)</span>;
</code></pre>
<p>The convention is clear, and the mapping is simple - less code to write. Its a big win to use the library!</p>
<h2 id="heading-the-source-of-hate">The source of hate</h2>
<p>I've personally used AutoMapper for a long time now. Its included in many popular templates, like many of the Clean Architecture templates that long dominate the .NET Enterprise space.</p>
<p>A lot of the hate is chalked up to the misuse of the library, which Jimmy breaks down greatly in the linked blog post. I won't comment further on that, as he does a great job.</p>
<p>The second main reason that goes around is performance. Yes - it uses reflection, and that is slower than manual/source generated code. That is a valid reason.</p>
<p>The final reason, which is the main one for me, is that it reduces the ability for static analysis between code. This is a big one in the Vertical Slice Architecture space, and that is what I spend a lot of time working within. Optimising for developer confidence, and refactor-ability.</p>
<p>Now what do I mean by that?</p>
<h3 id="heading-context-on-a-sample-application">Context on a Sample application</h3>
<p>For the rest of the blog post, I will use a very simple sample application, using the following Domain entity that will be mapped into DTOs using different methods.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">WeatherForecast</span>
{
    <span class="hljs-keyword">public</span> DateOnly Date { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> TemperatureC { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Summary { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<blockquote>
<p>Note: All the code from this article is available on my <a target="_blank" href="https://github.com/Hona/ReferenceCode/tree/main/AutoMapperBlog">GitHub here 🧑‍💻</a></p>
</blockquote>
<p>Each mapping sample has its own copy of the following DTO, to look into the static analysis that an IDE can see.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">WeatherForecastDto</span>
{
    <span class="hljs-keyword">public</span> DateOnly Date { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> TemperatureC { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> TemperatureF =&gt; <span class="hljs-number">32</span> + (<span class="hljs-keyword">int</span>)(TemperatureC / <span class="hljs-number">0.5556</span>);
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Summary { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p>We could even make it so that the TemperatureF is a calculated property from mapping configuration, but I want to keep it really simple.</p>
<h3 id="heading-reduced-static-analysis">Reduced static analysis</h3>
<p>To put it very quickly, because the library is implemented using reflection, the links between objects and properties is not clear.</p>
<p>To configure AutoMapper, we simply add this line to a mapping configuration (probably through a <code>Profile</code>)</p>
<pre><code class="lang-csharp">CreateMap&lt;WeatherForecast, WeatherForecastDto&gt;();
</code></pre>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fautomapper%2Fautomapper-missing-usages.png&amp;w=1080&amp;q=75" alt="No Usages found for properties inside of Rider" /></p>
<p>In this example, we can see that none of the fields are show have any Usages. This means it can't find where it is set from. When I refactor in an area I don't know well, potentially remove fields from the Domain entity, or even rename them - I have no confidence that the mapping will still work.</p>
<p>This is a big problem for me, and I know it is for others too.</p>
<h2 id="heading-what-can-i-do-otherwise">What can I do otherwise?</h2>
<p>Yes, you can manually map each property with AutoMapper, but that is directly counter to the point of the library.</p>
<h3 id="heading-manual-mapping">Manual Mapping</h3>
<p>Okay, we want a more safe codebase with static analysis, how about we just manually map the properties?</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Mapping</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> WeatherForecastDto <span class="hljs-title">ToDto</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> WeatherForecast entity</span>)</span>
    {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> WeatherForecastDto
        {
            Date = entity.Date,
            TemperatureC = entity.TemperatureC,
            Summary = entity.Summary
        };
    }
}
</code></pre>
<p>Now with this, we get the following static analysis in the IDE:</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fautomapper%2Fmanual-cohesion.png&amp;w=1920&amp;q=75" alt="Usages found for properties inside of Rider" /></p>
<p>That's pretty good! I can see where the properties are being used, and I can refactor with confidence.</p>
<p>However, we feel the same pain that caused Jimmy to write AutoMapper. For a lot of mapping, this is a lot of code!</p>
<h3 id="heading-mapperly">Mapperly</h3>
<p>This is where I want to introduce <a target="_blank" href="https://mapperly.riok.app/">Mapperly</a>. Similar to my AutoMapper introduction, here is a quote from the public site:</p>
<blockquote>
<p>A .NET source generator for generating object mappings. No runtime reflection.</p>
</blockquote>
<p>That sounds pretty awesome. A source generator, if you don't know generates code at compile time. The mapper wil basically create the mapping we did in the Manual section, but without us having to write it.</p>
<p>Let's set it up!</p>
<p>There are several ways to do it, however I like the static extension method syntax.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">Mapper</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Mapper</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">partial</span> WeatherForecastDto <span class="hljs-title">ToDto</span>(<span class="hljs-params">WeatherForecast entity</span>)</span>;
}
</code></pre>
<p>That's it - inspecting the source generated output, we can see the generated code is very similar:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// &lt;auto-generated /&gt;</span>

...

[<span class="hljs-meta">global::System.CodeDom.Compiler.GeneratedCode(<span class="hljs-meta-string">"Riok.Mapperly"</span>, <span class="hljs-meta-string">"3.6.0.0"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">global</span>::App.Mapperly.<span class="hljs-function">WeatherForecastDto <span class="hljs-title">ToDto</span>(<span class="hljs-params"><span class="hljs-keyword">global</span>::Domain.WeatherForecast entity</span>)</span>
{
    <span class="hljs-keyword">var</span> target = <span class="hljs-keyword">new</span> <span class="hljs-keyword">global</span>::App.Mapperly.WeatherForecastDto();
    target.Date = entity.Date;
    target.TemperatureC = entity.TemperatureC;
    target.Summary = entity.Summary;
    <span class="hljs-keyword">return</span> target;
}
</code></pre>
<p>In this sample comparison I didn't use extension method syntax, because of many conflicting namespaces. So for this case I called the map method like so: <code>var mapperlyDto = App.Mapperly.Mapper.ToDto(weatherForecast);</code></p>
<p>I would suggest using the extension method syntax, as it is much cleaner...</p>
<p>Anyway, as you would expect the static analysis is the same as the manual mapping.</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fautomapper%2Fmapperly-cohesion.png&amp;w=1080&amp;q=75" alt="Usages found for properties inside of Rider" /></p>
<p>Although it has the same great static analysis, we have the added benefit that we don't have to write the mapping code! This is the best of both worlds, convention based mapping, but manual mapping's static analysis!</p>
<h2 id="heading-what-about-performance">What about performance?</h2>
<p>Of course, we know AutoMapper is slow, but how slow? How does Mapperly compare to manual mapping?</p>
<p>I wrote a simple benchmark to compare. The whole snippet is this:</p>
<p><code>Program.cs</code></p>
<pre><code class="lang-csharp">BenchmarkRunner.Run&lt;Benchmarks&gt;();

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Benchmarks</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> WeatherForecast DomainEntity = <span class="hljs-keyword">new</span> WeatherForecast
    {
        Date = <span class="hljs-keyword">new</span> DateOnly(<span class="hljs-number">2021</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>),
        TemperatureC = <span class="hljs-number">25</span>,
        Summary = <span class="hljs-string">"Mild"</span>
    };

    [<span class="hljs-meta">Benchmark(Baseline = true)</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">Manual</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> dto = DomainEntity.ToDto();

        <span class="hljs-keyword">return</span> dto.TemperatureF;
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> IMapper Mapper = <span class="hljs-keyword">new</span> MapperConfiguration(cfg =&gt; cfg.AddProfile&lt;App.AutoMapper.Mapping&gt;()).CreateMapper();

    [<span class="hljs-meta">Benchmark</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">AutoMapper</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> dto = Mapper.Map&lt;App.AutoMapper.WeatherForecastDto&gt;(DomainEntity);

        <span class="hljs-keyword">return</span> dto.TemperatureF;
    }

    [<span class="hljs-meta">Benchmark</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">Mapperly</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> dto = App.Mapperly.Mapper.ToDto(DomainEntity);

        <span class="hljs-keyword">return</span> dto.TemperatureF;
    }
}
</code></pre>
<p>The results come back very clear.</p>
<p>Note - I marked the manual mapping as the baseline. That means both Mapperly and AutoMapper get compared to the manual mapping.</p>
<pre><code class="lang-bash">BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.3880/23H2/2023Update/SunValley3)
Intel Core i7-14700K, 1 CPU, 28 logical and 20 physical cores
.NET SDK 8.0.303
  [Host]     : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2


| Method     | Mean      | Error     | StdDev    | Ratio | RatioSD |
|----------- |----------:|----------:|----------:|------:|--------:|
| Manual     |  4.094 ns | 0.0565 ns | 0.0501 ns |  1.00 |    0.02 |
| AutoMapper | 35.244 ns | 0.1533 ns | 0.1280 ns |  8.61 |    0.11 |
| Mapperly   |  4.077 ns | 0.0278 ns | 0.0371 ns |  1.00 |    0.01 |
</code></pre>
<p>Awesome - Mapperly is as fast as manual mapping, and AutoMapper is 8.61 times slower!</p>
<p>Note: The performance here is within nanoseconds, so for most applications that will be negligible. I don't see it as a key factor in this case to pick Mapperly over AutoMapper.</p>
<p>This really proves that modern mapping libraries that use source generators are the way to go.</p>
<h2 id="heading-ef-core-projections">EF Core projections</h2>
<p>Once you start building real world apps inside of an AutoMapper codebase, a great feature is automatically projecting SQL queries into DTOs. It reduces the amount of fields coming back over the wire, and can be a big performance win.</p>
<p>How does this work in AutoMapper?</p>
<p>Take the following example DbContext:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">WeatherForecastContext</span> : <span class="hljs-title">DbContext</span>
{
    <span class="hljs-keyword">public</span> DbSet&lt;WeatherForecast&gt; WeatherForecasts { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</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.UseInMemoryDatabase(<span class="hljs-string">"WeatherForecast"</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">OnModelCreating</span>(<span class="hljs-params">ModelBuilder modelBuilder</span>)</span>
    {
        modelBuilder.Entity&lt;WeatherForecast&gt;().HasKey(e =&gt; e.Date);
    }
}
</code></pre>
<p>Let's say we want to query a slim version of <code>WeatherForecast</code> data.</p>
<p>The DTO would look like so:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">WeatherProjection</span>(<span class="hljs-params">DateOnly Date, <span class="hljs-keyword">int</span> TemperatureC</span>)</span>;
</code></pre>
<p>Using aggressive inlining (again, in a real app you would be using <code>Profile</code>s)</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IConfigurationProvider _automapperConfig = <span class="hljs-keyword">new</span> MapperConfiguration(cfg =&gt; cfg.CreateMap&lt;WeatherForecast, WeatherProjection&gt;());
</code></pre>
<p>Using the AutoMapper configuration we setup, we can project any query into the DTO.</p>
<p>Lets grab the first record.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> <span class="hljs-title">AutoMapperProjection</span>(<span class="hljs-params"></span>)</span>
{
    <span class="hljs-keyword">var</span> output = _context.WeatherForecasts
        .AsQueryable()
        .ProjectTo&lt;WeatherProjection&gt;(_automapperConfig)
        .First();

    <span class="hljs-keyword">return</span> output.ToString();
}
</code></pre>
<p>Okay that's pretty awesome!</p>
<p>We really like this feature, and we want to use it with Mapperly.</p>
<p>Well the good news is you can do this!</p>
<p>First, lets create the mapper.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">Mapper</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">WeatherProjectionMapper</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">partial</span> IQueryable&lt;EfCoreSample.WeatherProjection&gt; Project(<span class="hljs-keyword">this</span> IQueryable&lt;WeatherForecast&gt; q);
}
</code></pre>
<p>Now we can use the following code to project the query very similarly to AutoMapper:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> <span class="hljs-title">MapperlyProjection</span>(<span class="hljs-params"></span>)</span>
{
    <span class="hljs-keyword">var</span> output = _context.WeatherForecasts
        .AsQueryable()
        .Project()
        .First();

    <span class="hljs-keyword">return</span> output.ToString();
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<ul>
<li><p>AutoMapper is great to reduce code to write, but people misuse it, its slow (comparatively), and reduces static analysis (big deal)</p>
</li>
<li><p>Manual mapping is great for static analysis, but can be a lot of code to write.</p>
</li>
<li><p>Mapperly is the best of both worlds, and is as fast as manual mapping. ⭐</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In my opinion a <strong>significant </strong>number of projects should just be using manual mapping. There is a certain amount of ‘magic’ that comes with these packages and should be added with reluctance and an ADR.</div>
</div>

<p>I hope you enjoyed this blog post, and I hope you give Mapperly a try in your next project!</p>
<p>If you like the sound of Mapperly, then good news because its part of my <a target="_blank" href="https://github.com/Hona/VerticalSliceArchitecture">Vertical Slice Architecture template</a>!</p>
<p>Finally, find the complete sample code here: <a target="_blank" href="https://github.com/Hona/ReferenceCode/tree/main/AutoMapperBlog">https://github.com/Hona/ReferenceCode/tree/main/AutoMapperBlog</a></p>
]]></content:encoded></item><item><title><![CDATA[Tailwind & Blazor: Making it work with scoped CSS]]></title><description><![CDATA[The Problem
A little while ago I was wanting to find a way to allow my PostCSS Tailwind styles to be close to my Razor components.
Without any additional work, you might end up building styles & components in the following structure:
📁 BlazorApp/
│
...]]></description><link>https://lukeparker.dev/blazor-scoped-css-with-tailwind</link><guid isPermaLink="true">https://lukeparker.dev/blazor-scoped-css-with-tailwind</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 21 Dec 2023 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-the-problem">The Problem</h2>
<p>A little while ago I was wanting to find a way to allow my PostCSS Tailwind styles to be close to my Razor components.</p>
<p>Without any additional work, you might end up building styles &amp; components in the following structure:</p>
<pre><code class="lang-plaintext">📁 BlazorApp/
│
│── 📁 Shared/ 
│   │
│   │── MyButton.razor
|
│── 📁 Styles/ 
    │── 📄 MyButton.css
</code></pre>
<p><code>MyButton.razor</code></p>
<pre><code class="lang-plaintext">&lt;button class="bg-amber-800" @onclick="IncrementCount"&gt;Click me&lt;/button&gt;
</code></pre>
<p><code>MyButton.css</code></p>
<pre><code class="lang-css">﻿<span class="hljs-selector-tag">button</span> {
    @apply rounded-xl p-8;
    <span class="hljs-attribute">width</span>: <span class="hljs-built_in">calc</span>(<span class="hljs-number">100%</span> - <span class="hljs-number">2rem</span>);
}
</code></pre>
<p>This omits the other files like <code>tailwind.config.js</code> that come from a project setup.</p>
<blockquote>
<p>Checkout Chris Sainty's <a target="_blank" href="https://chrissainty.com/adding-tailwind-css-v3-to-a-blazor-app/">awesome tutorial</a> for setting up Tailwind in a Blazor project. You can imagine this blog post as an extension to that tutorial.</p>
</blockquote>
<p>Note that inline styles already work if you followed Chris's setup, however when we need to get a bit more power, like writing styles once &amp; using it many times, or mixing tailwind with typical css (e.g. the width calculation above), this approach falls short.</p>
<p>So, back to the structure. There are two problems:</p>
<ul>
<li><p>When I want to change <code>MyButton</code> I've got the files all scattered about. This violates the <a target="_blank" href="https://kula.blog/posts/proximity_principle/">proximity principle</a> and makes it harder to maintain - especially as your app grows.</p>
</li>
<li><p>Writing styles that get built using PostCSS into a published CSS file means that you don't gain the benefits of scoped CSS. You have to manually make sure there are no selectors that clash with other components.</p>
</li>
<li><p>You can't use @apply or write mixed CSS &amp; Tailwind styles.</p>
</li>
</ul>
<h2 id="heading-the-solution">The Solution</h2>
<p>Let's do better now.</p>
<p>First, lets create a SCSS file next to the component.</p>
<pre><code class="lang-plaintext">📁 Shared/ 
│── MyButton.razor
│── MyButton.razor.scss
</code></pre>
<p>This is not going to work out of the box, yet.</p>
<p>The first step is to extend the <code>tailwind.config.js</code> file to find classes in any razor file or SCSS file.</p>
<pre><code class="lang-js"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">content</span>: [<span class="hljs-string">"./**/*.{razor,css,scss,cs,html,js}"</span>], <span class="hljs-comment">// 👈 Add SCSS &amp; Razor files</span>
  <span class="hljs-attr">theme</span>: {
    <span class="hljs-attr">extend</span>: {},
  },
  <span class="hljs-attr">plugins</span>: [],
}
</code></pre>
<p>This is a nice glob of all things that could include a Tailwind class.</p>
<p>We have to make sure that SCSS compilation works for all users no matter the IDE or extensions. Previously for SCSS a lot of users would have to install an extension to get it to work. e.g. <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.SassCompiler">this one</a> for Visual Studio.</p>
<p>We actually can make this work in the MSBuild process meaning any IDE or even the command line will work.</p>
<p>In your csproj file, e.g. <code>BlazorApp.csproj</code> add the following:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">Project</span> <span class="hljs-attr">Sdk</span>=<span class="hljs-string">"Microsoft.NET.Sdk.BlazorWebAssembly"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">PropertyGroup</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">TargetFramework</span>&gt;</span>net7.0<span class="hljs-tag">&lt;/<span class="hljs-name">TargetFramework</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Nullable</span>&gt;</span>enable<span class="hljs-tag">&lt;/<span class="hljs-name">Nullable</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">ImplicitUsings</span>&gt;</span>enable<span class="hljs-tag">&lt;/<span class="hljs-name">ImplicitUsings</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">PropertyGroup</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">ItemGroup</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">PackageReference</span> <span class="hljs-attr">Include</span>=<span class="hljs-string">"BlazorComponentUtilities"</span> <span class="hljs-attr">Version</span>=<span class="hljs-string">"1.8.0"</span> /&gt;</span>
        ...
    <span class="hljs-tag">&lt;/<span class="hljs-name">ItemGroup</span>&gt;</span>

    <span class="hljs-comment">&lt;!-- 👇 New Code --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Target</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"StylesCompile"</span> <span class="hljs-attr">BeforeTargets</span>=<span class="hljs-string">"BeforeBuild"</span>&gt;</span>
        <span class="hljs-comment">&lt;!-- On Error, write the stderr as a build error --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Exec</span> <span class="hljs-attr">ConsoleToMSBuild</span>=<span class="hljs-string">"true"</span> <span class="hljs-attr">Command</span>=<span class="hljs-string">"npm run build:scoped-css"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Exec</span> <span class="hljs-attr">Command</span>=<span class="hljs-string">"npm run build:css"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Target</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- 👆 New Code --&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">Project</span>&gt;</span>
</code></pre>
<p>This allows us to run an NPM script BEFORE any files are added to the build process. You might be able to figure out where this is headed.</p>
<p>Now we need to add the <code>package.json</code> that contains the scripts <code>build:scoped-css</code> and <code>build:css</code></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-comment">// ⚠️ Versions may vary</span>
    <span class="hljs-attr">"autoprefixer"</span>: <span class="hljs-string">"^10.4.13"</span>,
    <span class="hljs-attr">"postcss"</span>: <span class="hljs-string">"^8.4.19"</span>,
    <span class="hljs-attr">"postcss-cli"</span>: <span class="hljs-string">"^10.0.0"</span>,
    <span class="hljs-attr">"tailwindcss"</span>: <span class="hljs-string">"^3.2.4"</span>,
    <span class="hljs-attr">"glob"</span>: <span class="hljs-string">"^8.0.3"</span>
  },
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"build:scoped-css"</span>: <span class="hljs-string">"node build-scoped-css.js"</span>,
    <span class="hljs-attr">"build:css"</span>: <span class="hljs-string">"postcss wwwroot/css/app.scss -o wwwroot/css/app.min.css"</span>
  }
}
</code></pre>
<p>Note the differences between the scripts.</p>
<ul>
<li><p><code>build:scoped-css</code> runs a custom script <code>build-scoped-css.js</code> that we will create in a moment.</p>
</li>
<li><p><code>build:css</code> processes the single 'global' SCSS file into a single CSS file. This is nothing special - however, if we're adding SCSS we might as well make it consistent.</p>
</li>
</ul>
<p>It's magic time! Create a file called <code>build-scoped-css.js</code> in the root of your project.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> glob = <span class="hljs-built_in">require</span>(<span class="hljs-string">'glob'</span>);
<span class="hljs-keyword">const</span> {exec} = <span class="hljs-built_in">require</span>(<span class="hljs-string">'child_process'</span>);

<span class="hljs-comment">// 👇 Find every scoped SCSS file</span>
glob(<span class="hljs-string">"**/*.razor.scss"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">er, files</span>) </span>{
    files.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file</span>) </span>{
        <span class="hljs-comment">// 👇 Output the processed file as *.razor.css, which is a normal scoped CSS file</span>
        <span class="hljs-keyword">const</span> command = <span class="hljs-string">`npx postcss "<span class="hljs-subst">${file}</span>" -o "<span class="hljs-subst">${file.replace(<span class="hljs-string">'.razor.scss'</span>, <span class="hljs-string">'.razor.css'</span>)}</span>"`</span>;
        <span class="hljs-built_in">console</span>.log(command)
        exec(command, <span class="hljs-function">(<span class="hljs-params">error, stdout, stderr</span>) =&gt;</span> {
            <span class="hljs-keyword">if</span> (error) {
                <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`exec error: <span class="hljs-subst">${error}</span>`</span>);
                <span class="hljs-keyword">return</span>;
            }
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`stdout: <span class="hljs-subst">${stdout}</span>`</span>);
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`stderr: <span class="hljs-subst">${stderr}</span>`</span>);
        });
    });
})
</code></pre>
<p>Make sure your index.html file includes both the Scoped CSS file and the global CSS file.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"css/app.min.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"BlazorApp.styles.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
</code></pre>
<p>To summarise what we just added, a few steps occur.</p>
<ol>
<li><p>Before running any Blazor build steps we run 2 scripts</p>
</li>
<li><p>The first script outputs *.razor.css files for every *.razor.scss file</p>
</li>
<li><p>The second script processes the global SCSS file into a single CSS file</p>
</li>
<li><p>The Blazor build process then runs as normal, with the working CSS files included</p>
</li>
</ol>
<p>🥳 That is all it takes!</p>
<p>If you want a complete example to follow rather than a step by step, here is my repository that setup an example project with these steps completed:</p>
<p>⭐ https://github.com/Hona/Blazor.Tailwind</p>
<p>It features a demo website on GitHub pages that you can explore also.</p>
]]></content:encoded></item><item><title><![CDATA[Vertical Slice Architecture: Quick start with .NET 8]]></title><description><![CDATA[Introduction
Vertical Slice Architecture (VSA) is a great way to structure your code. Getting started with a new or familiar architecture can be daunting, so I've created a quick start guide to get you up and running by using a dotnet new template.
W...]]></description><link>https://lukeparker.dev/vertical-slice-architecture-quick-start</link><guid isPermaLink="true">https://lukeparker.dev/vertical-slice-architecture-quick-start</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Sun, 22 Oct 2023 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Vertical Slice Architecture (VSA) is a great way to structure your code. Getting started with a new or familiar architecture can be daunting, so I've created a quick start guide to get you up and running by using a <code>dotnet new</code> template.</p>
<h3 id="heading-what-is-vertical-slice-architecture">What is Vertical Slice Architecture?</h3>
<p>VSA is primarily about separating use cases or features entirely, in such a way that they do not rely or communicate on each other. This is reflected via the 'vertical' nature of the architecture, where each feature is a vertical slice of the application.</p>
<p>Compared to traditional structures like N-tier or even Clean Architecture, where they are organised in ways of grouping by technical concern (Web API, Data Access), VSA groups by the feature or use case.</p>
<p>To get a quick briefing on a few of the key players in Software Architecture watch my User Group here:</p>
<p><a target="_blank" href="https://www.youtube.com/watch?v=dMgj1MdwrRE">Vertical Slice Architecture: How Does it Compare to Clean Architecture</a></p>
<p>This theory is all well and good, but how do I represent this in a .NET Application?</p>
<h3 id="heading-the-template">The Template</h3>
<p><a target="_blank" href="https://github.com/Hona/VerticalSliceArchitecture">https://github.com/Hona/VerticalSliceArchitecture</a></p>
<p>The template is a simple .NET 8 template that creates a solution with 1 source project (ASP.NET Core) &amp; 1 test project (xUnit).</p>
<p>The source project is setup with a few important folders to get you started:</p>
<pre><code class="lang-plaintext">📁 [ProjectName]/
│
│── 📁 Features/ 
│   │
│   │── 📁 Todo/
│       │── 🤔
|
│── 📁 Common/ 
    │── 📄 AppDbContext.cs
    │── 📄 *
</code></pre>
<p>What is inside each feature folder however?</p>
<p>This is where a little magic happens.</p>
<pre><code class="lang-plaintext">📁 Todo/
│
│── 📁 CreateTodo/
│   │── 📄 CreateTodoEndpoint.cs
│   │── 📄 CreateTodoRequest.cs
|
|── 📁 GetTodo/
|   │── 📄 *
|
|── 📁 [...]
|
|── 📄 TodoEntity.cs
|── 📄 TodoRepository.cs
|── 📄 DependencyInjection.cs
</code></pre>
<p>From here, as you can see - each feature contains an entity &amp; repository for each use case to consume. As well as a Dependency Injection class to wire up services to the IoC container.</p>
<p>This is a great way to get started with VSA, and I hope you find it useful!</p>
<p>Bigger enterprise apps will need more features &amp; different things to consider, like a Modular Monolith. But, that is for another time.</p>
<h3 id="heading-how-to-use-the-template">How to use the template</h3>
<p>To use the template, you need to install it first. This is done via the <code>dotnet new</code> command.</p>
<pre><code class="lang-bash">dotnet new install Hona.VerticalSliceArchitecture.Template
</code></pre>
<p>Then, create a project (e.g. called 'Sprout')</p>
<pre><code class="lang-bash">mkdir Sprout
<span class="hljs-built_in">cd</span> Sprout

dotnet new hona-vsa
</code></pre>
<p>Let me know your thoughts below, or if I should add anything else to the template!</p>
]]></content:encoded></item><item><title><![CDATA[Rapid Backend Development with Marten DB]]></title><description><![CDATA[Introduction
Building a personal project can be exciting, but it can also be a lot of work. In this post, I will go over how to use Marten DB to quickly build a backend for your project.
The Problem
When building a project, you will often need to bui...]]></description><link>https://lukeparker.dev/rapid-development-with-marten-db</link><guid isPermaLink="true">https://lukeparker.dev/rapid-development-with-marten-db</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Tue, 13 Sep 2022 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Building a personal project can be exciting, but it can also be a lot of work. In this post, I will go over how to use Marten DB to quickly build a backend for your project.</p>
<h3 id="heading-the-problem">The Problem</h3>
<p>When building a project, you will often need to build a backend for it. This can be a lot of work, and it's easy to get distracted by the infrastructure and forget about the project. In this post, I will go over how to use Marten DB to quickly build a backend for your project.</p>
<h3 id="heading-the-solution">The Solution</h3>
<p>Marten DB is a fantastic document store that uses PostgreSQL as the database engine. It is easy to use, and allows you to quickly build your backend.</p>
<h2 id="heading-martens-background">Marten's Background</h2>
<p>Before Marten came around, the de facto Document DB to use was Raven DB. Raven DB was a fantastic Document DB, but it was not free.</p>
<p>The developers behind Marten were working on a 'very large web application' that was suffering with performance and stability issues - that was using RavenDB as the document store. They decided to build their own document store, and Marten was born.</p>
<p>But why the name?</p>
<p>Straight from the Marten DB website:</p>
<blockquote>
<p>The project name Marten came from a quick Google search one day for "what are the natural predators of ravens?" -- which led to us to use the marten as our project codename and avatar.</p>
</blockquote>
<p>A bit of a superhero origin story, but it's a great name!</p>
<h3 id="heading-setting-up-marten-db">Setting up Marten DB</h3>
<p>First things first lets create a new ASP.NET Core Web API, and add MediatR to it. While we are at it, let's also add Marten.</p>
<pre><code class="lang-bash">dotnet new webapi -o MartenTest
<span class="hljs-built_in">cd</span> MartenTest
dotnet add package Marten
dotnet add package MediatR
</code></pre>
<p>Now we need to add a connection string to our appsettings.json file. I am using a local PostgreSQL database, but you can use any version Marten supports, which as of right now is: <code>PostgreSQL 9.6 or above database with PLV8</code></p>
<p><code>appsettings.Development.json</code></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"ConnectionStrings"</span>: {
    <span class="hljs-attr">"MartenTest"</span>: <span class="hljs-string">"Host=localhost;Database=MartenTest;Username=postgres;Password=postgres"</span>
  }
}
</code></pre>
<p>Next lets setup the pipeline in <code>Program.cs</code></p>
<pre><code class="lang-cs">builder.Services.AddMarten(options =&gt;
{
    options.Connection(builder.Configuration.GetConnectionString(<span class="hljs-string">"Marten"</span>));

    options.AutoCreateSchemaObjects = builder.Environment.IsDevelopment()
        ? AutoCreate.All
        : AutoCreate.CreateOrUpdate;
});

<span class="hljs-comment">// For the sake of this article we'll just put everything into one project</span>
builder.Services.AddMediatR(<span class="hljs-keyword">typeof</span>(Program).Assembly);
</code></pre>
<p>As a homage to the Blazor template using WeatherForecasts as the model of choice, lets build a few commands and queries to perform CRUD operations on.</p>
<p><code>MartenTest/Domain/WeatherForecast.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">WeatherForecast</span>(<span class="hljs-params">DateTime Date, <span class="hljs-keyword">int</span> TemperatureC, <span class="hljs-keyword">string</span> Summary</span>)</span>;
</code></pre>
<p>Now, if you've used Document Stores to any degree before - we need to have a field that acts as the identity or primary key. Marten DB by default looks for the <code>Id</code> property, so we need to manually configure it to use <code>Date</code> instead.</p>
<p><code>MartenTest/Program.cs</code></p>
<pre><code class="lang-cs">builder.Services.AddMarten(options =&gt;
{
    ...

    options.Schema.For&lt;WeatherForecast&gt;()
        .Identity(x =&gt; x.Date);
});
</code></pre>
<h4 id="heading-create">CREATE</h4>
<p><code>MartenTest/Application/CreateWeatherForecastCommand.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">CreateWeatherForecastCommand</span>(<span class="hljs-params">WeatherForecast WeatherForecast</span>) : IRequest&lt;WeatherForecast&gt;</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">CreateWeatherForecastCommandHandler</span> : <span class="hljs-title">IRequestHandler</span>&lt;<span class="hljs-title">CreateWeatherForecastCommand</span>, <span class="hljs-title">WeatherForecast</span>&gt;
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDocumentSession _documentSession;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">CreateWeatherForecastCommandHandler</span>(<span class="hljs-params">IDocumentSession documentSession</span>)</span>
    {
        _documentSession = documentSession;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;WeatherForecast&gt; <span class="hljs-title">Handle</span>(<span class="hljs-params">CreateWeatherForecastCommand request, CancellationToken cancellationToken</span>)</span>
    {
        <span class="hljs-keyword">await</span> _documentSession.Insert(request.WeatherForecast);
        <span class="hljs-keyword">await</span> _documentSession.SaveChangesAsync(cancellationToken);

        <span class="hljs-keyword">return</span> request.WeatherForecast;
    }
}
</code></pre>
<h4 id="heading-update">UPDATE</h4>
<p><code>MartenTest/Application/UpdateWeatherForecastCommand.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">UpdateWeatherForecastCommand</span>(<span class="hljs-params">WeatherForecast WeatherForecast</span>) : IRequest&lt;WeatherForecast&gt;</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">UpdateWeatherForecastCommandHandler</span> : <span class="hljs-title">IRequestHandler</span>&lt;<span class="hljs-title">UpdateWeatherForecastCommand</span>, <span class="hljs-title">WeatherForecast</span>&gt;
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDocumentSession _documentSession;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UpdateWeatherForecastCommandHandler</span>(<span class="hljs-params">IDocumentSession documentSession</span>)</span>
    {
        _documentSession = documentSession;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;WeatherForecast&gt; <span class="hljs-title">Handle</span>(<span class="hljs-params">UpdateWeatherForecastCommand request, CancellationToken cancellationToken</span>)</span>
    {
        _documentSession.Store(request.WeatherForecast);
        <span class="hljs-keyword">await</span> _documentSession.SaveChangesAsync(cancellationToken);

        <span class="hljs-keyword">return</span> request.WeatherForecast;
    }
}
</code></pre>
<blockquote>
<p>An important thing to note is the nomenclature. You would use <code>Insert</code> to create a new document, and <code>Store</code> to update or create an existing document.</p>
</blockquote>
<h4 id="heading-delete">DELETE</h4>
<p><code>MartenTest/Application/DeleteWeatherForecastCommand.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">DeleteWeatherForecastCommand</span>(<span class="hljs-params">DateTime Date</span>) : IRequest</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DeleteWeatherForecastCommandHandler</span> : <span class="hljs-title">IRequestHandler</span>&lt;<span class="hljs-title">DeleteWeatherForecastCommand</span>&gt;
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDocumentSession _documentSession;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DeleteWeatherForecastCommandHandler</span>(<span class="hljs-params">IDocumentSession documentSession</span>)</span>
    {
        _documentSession = documentSession;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;Unit&gt; <span class="hljs-title">Handle</span>(<span class="hljs-params">DeleteWeatherForecastCommand request, CancellationToken cancellationToken</span>)</span>
    {
        _documentSession.Delete&lt;WeatherForecast&gt;(<span class="hljs-keyword">new</span> WeatherForecast(request.Date, <span class="hljs-number">0</span>, <span class="hljs-string">""</span>));
        <span class="hljs-keyword">await</span> _documentSession.SaveChangesAsync(cancellationToken);

        <span class="hljs-keyword">return</span> Unit.Value;
    }
}
</code></pre>
<h4 id="heading-read">READ</h4>
<p><code>MartenTest/Application/GetWeatherForecastQuery.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-function"><span class="hljs-keyword">public</span> record <span class="hljs-title">GetWeatherForecastQuery</span>(<span class="hljs-params">DateTime Date</span>) : IRequest&lt;WeatherForecast&gt;</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">GetWeatherForecastQueryHandler</span> : <span class="hljs-title">IRequestHandler</span>&lt;<span class="hljs-title">GetWeatherForecastQuery</span>, <span class="hljs-title">WeatherForecast</span>&gt;
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDocumentSession _documentSession;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">GetWeatherForecastQueryHandler</span>(<span class="hljs-params">IDocumentSession documentSession</span>)</span>
    {
        _documentSession = documentSession;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> Task&lt;WeatherForecast&gt; <span class="hljs-title">Handle</span>(<span class="hljs-params">GetWeatherForecastQuery request, CancellationToken cancellationToken</span>)</span>
    {
        <span class="hljs-keyword">return</span> _documentSession.Query&lt;WeatherForecast&gt;()
            .FirstOrDefaultAsync(x =&gt; x.Date == request.Date, cancellationToken);
    }
}
</code></pre>
<p>Just like that we have a fully functional infrastructure. As a bonus - lets use minimal APIs to expose our CRUD operations.</p>
<blockquote>
<p>Minimal APIs are a new feature in .NET 6 that allows you to build web APIs without having to use controllers. You can read more about them <a target="_blank" href="https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#minimal-apis">here</a>.</p>
<p>MediatR is discussing adding support for minimal APIs <a target="_blank" href="https://github.com/jbogard/MediatR/issues/653">here</a>.</p>
</blockquote>
<p><code>MartenTest/WebApi/Endpoints.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Endpoints</span>
{
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MapEndpoints</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> WebApplication app</span>)</span>
    {
        app.MapGet(<span class="hljs-string">"/weatherforecast/{date}"</span>, (DateTime date, IMediator mediator, CancellationToken ct) =&gt;
            mediator.Send(<span class="hljs-keyword">new</span> GetWeatherForecastQuery(date), ct)
        );
        app.MapPost(<span class="hljs-string">"/weatherforecast"</span>, ([FromBody] model, IMediator mediator, CancellationToken ct) =&gt;
            mediator.Send(<span class="hljs-keyword">new</span> CreateWeatherForecastCommand(model), ct)
        );
        app.MapPut(<span class="hljs-string">"/weatherforecast"</span>, ([FromBody] model, IMediator mediator, CancellationToken ct) =&gt;
            mediator.Send(<span class="hljs-keyword">new</span> UpdateWeatherForecastCommand(model), ct)
        );
        app.MapDelete(<span class="hljs-string">"/weatherforecast/{date}"</span>, (DateTime date, IMediator mediator, CancellationToken ct) =&gt;
            mediator.Send(<span class="hljs-keyword">new</span> DeleteWeatherForecastCommand(date), ct)
        );
    }
}
</code></pre>
<p><code>MartenTest/Program.cs</code></p>
<pre><code class="lang-cs"><span class="hljs-keyword">var</span> app = builder.Build();

...

app.MapEndpoints();

app.Run();
</code></pre>
<p>And that's it! We have a fully functional CRUD API that uses Marten DB as the data store. </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Marten DB is a great tool for building Document Stores in .NET. It's easy to use, and has a lot of great features. I hope this article has helped you get started with Marten DB. If you have any questions or comments, feel free to reach out to me on Twitter <a target="_blank" href="https://twitter.com/LukeParkerDev">@LukeParkerDev</a>.</p>
<p>Please let me know what you think of Marten DB, or this article. I'd love to hear your feedback in the comments below!</p>
]]></content:encoded></item><item><title><![CDATA[Blazor WASM SEO - You have a broken website according to Google!]]></title><description><![CDATA[Introduction
Blazor WebAssembly is a great framework for building web applications. It's fast, easy to use, and has a great community. However, there is one thing that is often overlooked when building a Blazor WebAssembly application, and that is SE...]]></description><link>https://lukeparker.dev/blazor-wasm-seo</link><guid isPermaLink="true">https://lukeparker.dev/blazor-wasm-seo</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 08 Sep 2022 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Blazor WebAssembly is a great framework for building web applications. It's fast, easy to use, and has a great community. However, there is one thing that is often overlooked when building a Blazor WebAssembly application, and that is SEO. In this post, I will go over the problem and how to fix it.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>As we know Blazor WASM is a static site, however it requires JavaScript to run on the client and load the .NET Framework using WebAssembly. This means when a crawler like GoogleBot visits the site to index it &amp; load information the default index.html is served.</p>
<p>Basically the only text that is 'human readable' by default in this file is:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"blazor-error-ui"</span>&gt;</span>
    An unhandled error has occurred.
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">""</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"reload"</span>&gt;</span>Reload<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"dismiss"</span>&gt;</span>🗙<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>So what does that mean for your SEO?</p>
<p>Well, what google thinks your site is, is a broken website.</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Flukeparkerdev-bad-seo.png&amp;w=1080&amp;q=75" alt="Bad SEO - Google Search Result" /></p>
<h2 id="heading-the-quick-solution">The Quick Solution</h2>
<p>If you are just looking for general accuracy for your site, you can just add <code>&lt;meta&gt;</code> tags to the head of your index.html file. This will tell Google what your site is about, and will help with the accuracy of your search results.</p>
<p>An example of this is:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"keywords"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"Keywords"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"og:title"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"Title"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"twitter:title"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"Title"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"SubTitle"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"og:description"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"SubTitle"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"twitter:description"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"SubTitle"</span> /&gt;</span>
</code></pre>
<p>Much better than the error message you get by default!</p>
<p>However, this is not a great solution.</p>
<ol>
<li><p>It still won't load any of the page which helps with keywords and ranking metrics.</p>
</li>
<li><p>What if you have a blog?</p>
</li>
<li><p>What if you have subpages that should have descriptions?</p>
</li>
</ol>
<h2 id="heading-the-better-solution-prerendering-for-crawlers">The Better Solution - Prerendering for Crawlers</h2>
<p>The better solution is to prerender the pages for crawlers. This means that when a crawler visits the site, it will get a fully loaded and cached version of your site. This won't impact your users at all, and allows you to use a free hosting service like GitHub Pages, or Azure Static Web Apps.</p>
<p><strong>The Benefits</strong></p>
<ul>
<li><p>Custom <code>&lt;meta&gt;</code> tags and SEO data for each page. Completely customizable in your Razor components!</p>
</li>
<li><p>No impact on your users, as the prerendering is only for crawlers.</p>
</li>
</ul>
<h3 id="heading-prerendering-with-blazor">Prerendering with Blazor</h3>
<p>A prerequisite to this way of prerending is that you use Cloudflare for your DNS.</p>
<p>In my case lukeparker.dev is hosted on GitHub Pages &amp; CloudFlare pages, with CloudFlare as the DNS provider.</p>
<h4 id="heading-prerenderio-a-spa-prerendering-service-for-crawlers">Prerender.io - A SPA Prerendering Service for Crawlers</h4>
<blockquote>
<p>I am not sponsored or affiliated with prerender.io in any way. I use the free service and it works well.</p>
</blockquote>
<p><a target="_blank" href="https://prerender.io/">prerender.io</a> is a service that allows you to prerender your site for crawlers. It's free for up to 1000 prerenders per month, which is more than enough for most sites (roughly 250 unique pages).</p>
<p>It utilizes an industry standard for this problem which is Dynamic Rendering. Which, like I outlined is basically prerendering the site for crawlers, while serving the normal site for users.</p>
<h4 id="heading-setting-it-up">Setting it Up</h4>
<p>Thankfully instead of me maintaining a third party manual, they have a CloudFlare integration guide <a target="_blank" href="https://docs.prerender.io/docs/24-cloudflare">here</a>.</p>
<p>Basically you copy a little JavaScript code into a new Worker on CloudFlare &amp; setup your API key &amp; domain.</p>
<p>At that point you're good to go &amp; can start using prerender.io!</p>
<hr />
<h2 id="heading-blazor-seo-page-based-dynamic-metadata">Blazor SEO - Page-based Dynamic Metadata</h2>
<blockquote>
<p>Note: My implementation is heavily based on <a target="_blank" href="https://github.com/MudBlazor/MudBlazor/blob/dev/src/MudBlazor.Docs/Components/DocsPageHeader.razor#L10-L22">MudBlazor's</a> which is what prompted me to go down the SEO route with Blazor WASM.</p>
</blockquote>
<p>Firstly we need to create a component that will be used to set the metadata for each page. This is a simple component that will take in a title, description, and keywords.</p>
<p>Create a file called <code>SeoHeader.razor</code>.</p>
<p>Then lets fill it with the following:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">PageTitle</span>&gt;</span>@(GetTitle())<span class="hljs-tag">&lt;/<span class="hljs-name">PageTitle</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">HeadContent</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"keywords"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"@GetKeywords()"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"og:title"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"@GetTitle()"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"twitter:title"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"@GetTitle()"</span> /&gt;</span>
    @if (!string.IsNullOrEmpty(Overview))
    {
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"@GetSubTitle()"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Property</span>=<span class="hljs-string">"og:description"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"@GetSubTitle()"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">Name</span>=<span class="hljs-string">"twitter:description"</span> <span class="hljs-attr">Content</span>=<span class="hljs-string">"@GetSubTitle()"</span> /&gt;</span>
    }
<span class="hljs-tag">&lt;/<span class="hljs-name">HeadContent</span>&gt;</span>
</code></pre>
<p>Note the usage of <code>&lt;PageTitle&gt;</code> to set the tab title &amp; the <code>&lt;HeadContent&gt;</code> to set part of the <code>&lt;head&gt;</code> of the page using the new HeadOutlet.</p>
<p>Now we just need to fill out the code that takes these variables and returns the nice strings.</p>
<p>Add this code at the bottom of the component:</p>
<pre><code class="lang-cs">@code {
    [<span class="hljs-meta">Parameter</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Title { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    [<span class="hljs-meta">Parameter</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Overview { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    [<span class="hljs-meta">Parameter</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">string</span>&gt; Keywords</span> { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">string</span>&gt;();

    <span class="hljs-function"><span class="hljs-keyword">string</span> <span class="hljs-title">GetTitle</span>(<span class="hljs-params"></span>)</span> =&gt; Title <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span> ? <span class="hljs-string">"LukeParkerDev"</span> : <span class="hljs-string">$"<span class="hljs-subst">{Title}</span> | [YOUR BRAND NAME/SUFFIX]"</span>;

    <span class="hljs-function"><span class="hljs-keyword">string</span> <span class="hljs-title">GetSubTitle</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrEmpty(Overview))
            <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>;
        <span class="hljs-keyword">return</span> Overview.TrimEnd(<span class="hljs-string">'.'</span>) + <span class="hljs-string">"."</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">string</span> <span class="hljs-title">GetKeywords</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> keywords = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">string</span>&gt;();

        keywords.AddRange(Keywords);

        keywords.Add(<span class="hljs-string">"[YOUR NAME or BRAND]"</span>);

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.Join(<span class="hljs-string">", "</span>, keywords);
    }
}
</code></pre>
<p>At this point the component is ready to be consumed in every Razor Page.</p>
<h3 id="heading-using-the-seo-header-component">Using the SEO Header Component</h3>
<p>Starting simple, lets add it to the Index page.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">SeoHeader</span>
    <span class="hljs-attr">Overview</span>=<span class="hljs-string">"My personal website acting as a personal portfolio as well as a blog to share my knowledge."</span> 
    <span class="hljs-attr">Keywords</span>=<span class="hljs-string">"@(new []{"</span><span class="hljs-attr">blog</span>", "<span class="hljs-attr">portfolio</span>"})"/&gt;</span>
</code></pre>
<p>This one just sets a description &amp; keywords. Remember it always adds the one keyword of your brand name.</p>
<p>Now lets add it to something more dynamic, like a blog post, dynamically loaded</p>
<pre><code class="lang-html">@if (_loading)
{
    <span class="hljs-tag">&lt;<span class="hljs-name">MudProgressLinear</span> <span class="hljs-attr">Color</span>=<span class="hljs-string">"Color.Secondary"</span> <span class="hljs-attr">Indeterminate</span>/&gt;</span>
}
else if (_blog is not null)
{
    <span class="hljs-tag">&lt;<span class="hljs-name">SeoHeader</span> 
            <span class="hljs-attr">Title</span>=<span class="hljs-string">"@_blog.Frontmatter.title"</span> 
            <span class="hljs-attr">Overview</span>=<span class="hljs-string">"@_blog.Frontmatter.hook"</span> 
            <span class="hljs-attr">Keywords</span>=<span class="hljs-string">"_blog.Frontmatter.categories
                .Concat(_blog.Frontmatter.series)
                .Concat(_blog.Frontmatter.tags)"</span> /&gt;</span>
    ...
}
</code></pre>
<p>Thats all! Now your site will slowly get crawled by Google &amp; others, in a few days you'll see the updated version of your site!</p>
<h3 id="heading-the-result">The Result</h3>
<p>This exact site uses that approach, and here is an example of the result in Discord:</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fseo-result-discord.png&amp;w=1920&amp;q=75" alt="Good SEO - Discord Embed" /></p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fseo-result-google.png&amp;w=1920&amp;q=75" alt="Good SEO - Google" /></p>
<p>Even better is MudBlazor's, which has a lot more pages &amp; content:</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fseo-result-mudblazor-google.png&amp;w=1920&amp;q=75" alt="Good SEO - MudBlazor on Google" /></p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>I hope this post has helped you understand how to do SEO with Blazor WASM. If you have any questions, feel free to ask in the comments!</p>
<h3 id="heading-next-steps">Next Steps</h3>
<p>You might want to generate a SiteMap dynamically, or have one statically, which helps Google &amp; other crawlers find your pages much more easily! You can see how I do it <a target="_blank" href="https://github.com/Hona/LukeParkerDev/blob/main/LukeParkerDev.BuildJob/Program.cs">here</a> on my blog's Build Job.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Beautiful OKR with AntBlazor - Part 3]]></title><description><![CDATA[Setup AntBlazor
Before we can get started using the many great components provided by AntBlazor, we have to first install it and follow the setup steps.
NuGet
Using your package manager, add a reference to the latest version of AntDesign in the proje...]]></description><link>https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-3</link><guid isPermaLink="true">https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-3</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Wed, 25 Aug 2021 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-setup-antblazor">Setup AntBlazor</h2>
<p>Before we can get started using the many great components provided by <a target="_blank" href="https://antblazor.com">AntBlazor</a>, we have to first install it and follow the <a target="_blank" href="https://antblazor.com/en-US/docs/introduce">setup steps</a>.</p>
<h3 id="heading-nuget">NuGet</h3>
<p>Using your package manager, add a reference to the latest version of <code>AntDesign</code> in the project <code>OKR.Web</code></p>
<h3 id="heading-blazor-boilerplate">Blazor Boilerplate</h3>
<p>Now with the package installed, we have to reference the assets and add a few modules to the dependency injection.</p>
<p>Add to OKR.Web/Pages/_Host.cshtml, just before the <code>&lt;head&gt;</code> tag closes:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"_content/AntDesign/css/ant-design-blazor.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"_content/AntDesign/js/ant-design-blazor.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>This references the default CSS file - you can replace it to easily get <a target="_blank" href="https://ant.design/docs/react/customize-theme">dark mode</a>, or a custom theme. There is also some JS interop required hence the import.</p>
<p>OKR.Web/Startup.cs</p>
<pre><code class="lang-cs">services.AddAntDesign();
</code></pre>
<p>This is an extension method provided in the library, which adds the services all in one clean call. Some of the services added here is the message service, modal service, etc.</p>
<p>OKR.Web/App.razor</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">AntContainer</span> /&gt;</span>
</code></pre>
<p>At the bottom of the file add this line, this allows the popups to run globally and persist on page changes, as well as <a target="_blank" href="https://github.com/ant-design-blazor/ant-design-blazor/blob/master/components/core/AntContainer.razor">other uses</a>.</p>
<p>Finally, we need to add the AntBlazor namespace to the Blazor component imports:</p>
<p>OKR.Web/_Imports.razor</p>
<pre><code class="lang-cs">@using AntDesign
</code></pre>
<p>That's it! We are now ready to build with these enterprise grade components.</p>
<h3 id="heading-removing-bootstrap">Removing Bootstrap</h3>
<p>A small caveat of the Blazor template provided by .NET is that all projects begin with Bootstrap and Open Iconic. The CSS file conflicts with AntBlazor and causes some centring issues.</p>
<p>As we aren't using Bootstrap or Open Iconic in this project at all, we can just delete both of them completely:</p>
<ul>
<li><p>OKR.Web/wwwroot/css/bootstrap</p>
</li>
<li><p>OKR.Web/wwwroot/css/open-iconic</p>
</li>
</ul>
<h3 id="heading-removing-unused-css">Removing Unused CSS</h3>
<p>Now that there is no bootstrap, there was some custom CSS that we don't use, so lets clean it up.</p>
<p>OKR.Web/wwwroot/css/site.css</p>
<pre><code class="lang-css">  <span class="hljs-selector-id">#blazor-error-ui</span> {
      <span class="hljs-attribute">background</span>: lightyellow;
      <span class="hljs-attribute">bottom</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> -<span class="hljs-number">1px</span> <span class="hljs-number">2px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.2</span>);
      <span class="hljs-attribute">display</span>: none;
      <span class="hljs-attribute">left</span>: <span class="hljs-number">0</span>;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">0.6rem</span> <span class="hljs-number">1.25rem</span> <span class="hljs-number">0.7rem</span> <span class="hljs-number">1.25rem</span>;
      <span class="hljs-attribute">position</span>: fixed;
      <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
      <span class="hljs-attribute">z-index</span>: <span class="hljs-number">1000</span>;
  }

      <span class="hljs-selector-id">#blazor-error-ui</span> <span class="hljs-selector-class">.dismiss</span> {
          <span class="hljs-attribute">cursor</span>: pointer;
          <span class="hljs-attribute">position</span>: absolute;
          <span class="hljs-attribute">right</span>: <span class="hljs-number">0.75rem</span>;
          <span class="hljs-attribute">top</span>: <span class="hljs-number">0.5rem</span>;
      }
</code></pre>
<p>Delete everything from the file leaving just the Blazor error UI - which is the bar on the bottom.</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fblazor-error-ui.PNG&amp;w=3840&amp;q=75" alt /></p>
<h2 id="heading-using-forwarded-headers">Using Forwarded Headers</h2>
<p>As we are proxying from HTTPS/domain to the project using NGINX, the application needs to change the pipeline so that redirects are correct, and HTTPS is correctly detected. We also don't need to force redirects to HTTPS as it is already forced using NGINX.</p>
<p>Just before <code>app.UseStaticFiles();</code> add the following:</p>
<p>OKR.Web/Startup.cs</p>
<pre><code class="lang-cs">app.UseForwardedHeaders(<span class="hljs-keyword">new</span> ForwardedHeadersOptions
{
    ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
</code></pre>
<p>Then, delete the redirection: <code>app.UseHttpsRedirection();</code></p>
<h2 id="heading-verify-antblazor-is-working">Verify AntBlazor is Working</h2>
<p>Inside of Index.razor, there is currently a simple 'Hello World', lets test everything is working correctly by displaying a simple button:</p>
<p>OKR.Web/Pages/Index.razor</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">Button</span> <span class="hljs-attr">Type</span>=<span class="hljs-string">"primary"</span>&gt;</span>Primary<span class="hljs-tag">&lt;/<span class="hljs-name">Button</span>&gt;</span>
</code></pre>
<p>You should see the following:</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fantblazor-primary-button.PNG&amp;w=256&amp;q=75" alt /></p>
<p>If you got this far, you are doing great!</p>
<h2 id="heading-designing-the-main-layout">Designing the Main Layout</h2>
<p>The AntBlazor documentation is great, it provides <a target="_blank" href="https://antblazor.com/en-US/components/layout">many examples of how to setup a typical layout</a>.</p>
<p>For this project, at least initially we don't need a complex setup, but for future proofing we can include a menu bar, with navigation, as well as a simple content area for our page (anything with a route) components.</p>
<p>Lets replace the OKR.Web/Shared/MainLayout.razor with the following (using AntBlazor components)</p>
<pre><code class="lang-html">@inherits LayoutComponentBase

<span class="hljs-tag">&lt;<span class="hljs-name">Layout</span> <span class="hljs-attr">Style</span>=<span class="hljs-string">"min-height: 100vh; width: 100vw"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Header</span> <span class="hljs-attr">Style</span>=<span class="hljs-string">"display: flex; align-items: center;"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Title</span> <span class="hljs-attr">Level</span>=<span class="hljs-string">"2"</span> <span class="hljs-attr">Class</span>=<span class="hljs-string">"ant-menu-dark ant-menu-submenu-open"</span> <span class="hljs-attr">Style</span>=<span class="hljs-string">"padding: 0; margin: 0 12px 0 0; color: #ffffff"</span>&gt;</span>OKR<span class="hljs-tag">&lt;/<span class="hljs-name">Title</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Menu</span> <span class="hljs-attr">Theme</span>=<span class="hljs-string">"MenuTheme.Dark"</span> <span class="hljs-attr">Mode</span>=<span class="hljs-string">"MenuMode.Horizontal"</span> <span class="hljs-attr">DefaultSelectedKeys</span>=<span class="hljs-string">@(new[]{</span>"<span class="hljs-attr">2</span>"})&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">AntDesign.MenuItem</span> <span class="hljs-attr">Key</span>=<span class="hljs-string">"1"</span>&gt;</span>nav 1<span class="hljs-tag">&lt;/<span class="hljs-name">AntDesign.MenuItem</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">AntDesign.MenuItem</span> <span class="hljs-attr">Key</span>=<span class="hljs-string">"2"</span>&gt;</span>nav 2<span class="hljs-tag">&lt;/<span class="hljs-name">AntDesign.MenuItem</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">AntDesign.MenuItem</span> <span class="hljs-attr">Key</span>=<span class="hljs-string">"3"</span>&gt;</span>nav 3<span class="hljs-tag">&lt;/<span class="hljs-name">AntDesign.MenuItem</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">Menu</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Header</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">AntDesign.Content</span> <span class="hljs-attr">Style</span>=<span class="hljs-string">"padding: 0 50px;"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Breadcrumb</span> <span class="hljs-attr">Style</span>=<span class="hljs-string">"margin: 16px 0;"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">BreadcrumbItem</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">BreadcrumbItem</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">BreadcrumbItem</span>&gt;</span>List<span class="hljs-tag">&lt;/<span class="hljs-name">BreadcrumbItem</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">BreadcrumbItem</span>&gt;</span>App<span class="hljs-tag">&lt;/<span class="hljs-name">BreadcrumbItem</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">Breadcrumb</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>@Body<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">AntDesign.Content</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Footer</span> <span class="hljs-attr">Style</span>=<span class="hljs-string">"text-align: center;"</span>&gt;</span>©2021 Created by Luke Parker<span class="hljs-tag">&lt;/<span class="hljs-name">Footer</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">Layout</span>&gt;</span>
</code></pre>
<p>Your layout should look similar to this:</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fokr-layout.PNG&amp;w=3840&amp;q=75" alt /></p>
<p>At this point we have a consistent visual story without thinking and we can begin designing the OKR cards. For the dashboard, we should have an overview of each OKR. The perfect component for this is the <a target="_blank" href="https://antblazor.com/en-US/components/card">Card</a>.</p>
<p>Before we get started however, we need to build a view model to act as an aggregate object of the object and all the key results.</p>
<p>Create a file: OKR.Web/ViewModels/OKRViewModel</p>
<pre><code class="lang-cs"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OKRViewModel</span>
{
    <span class="hljs-keyword">public</span> Objective Objective { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> IReadOnlyList&lt;KeyResult&gt; KeyResults { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p>At this point we can assume that the OKR Card component will take a single <code>OKRViewModel</code> - where this data comes from right now doesn't matter.</p>
<p>Lets create OKR.Web/Shared/OKRCard.razor</p>
<pre><code class="lang-html">@using OKR.Web.ViewModels
<span class="hljs-tag">&lt;<span class="hljs-name">Card</span> <span class="hljs-attr">Bordered</span>=<span class="hljs-string">"true"</span> <span class="hljs-attr">Hoverable</span>=<span class="hljs-string">"true"</span> <span class="hljs-attr">Class</span>=<span class="hljs-string">"okr-card"</span>
      <span class="hljs-attr">Title</span>=<span class="hljs-string">"@(OKR?.Objective.Title ?? "</span><span class="hljs-attr">Loading...</span>")"&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Extra</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">AntDesign.Progress</span> <span class="hljs-attr">Type</span>=<span class="hljs-string">ProgressType.Circle</span> 
                  <span class="hljs-attr">Percent</span>=<span class="hljs-string">"@((int)_completion)"</span> 
                  <span class="hljs-attr">Size</span>=<span class="hljs-string">ProgressSize.Small</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Extra</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Body</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">AntList</span> <span class="hljs-attr">DataSource</span>=<span class="hljs-string">"@OKR.KeyResults"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ListItem</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">ListItemMeta</span> <span class="hljs-attr">Description</span>=<span class="hljs-string">"@context.Description"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">TitleTemplate</span>&gt;</span>
                        @context.Title
                    <span class="hljs-tag">&lt;/<span class="hljs-name">TitleTemplate</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">ListItemMeta</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"width: 35%"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">AntDesign.Progress</span> <span class="hljs-attr">Percent</span>=<span class="hljs-string">"@((int)(context.Completion * 100))"</span> <span class="hljs-attr">Size</span>=<span class="hljs-string">"@ProgressSize.Small"</span> /&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">ListItem</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">AntList</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">Card</span>&gt;</span>

@code {
    [Parameter]
    public OKRViewModel OKR
    {
        get =&gt; _okr;
        set
        {
            _okr = value;
            _completion = value.KeyResults.Average(keyResult =&gt; keyResult.Completion * 100);
        }
    }

    private decimal _completion;
    private OKRViewModel _okr;
}
</code></pre>
<p>Note that there is the <code>Class</code> property, instead of <code>class</code>. This is a custom AntBlazor property that passes the parameter to the real HTML element, which is very useful for custom styling.</p>
<p>Try running it, and you should get something that looks like this:</p>
<p><img src="https://luke-parker-dev-v3.vercel.app/_next/image?url=%2Fstatic%2Fimages%2Fblog%2Fokr-initial-card.PNG&amp;w=2048&amp;q=75" alt /></p>
<hr />
<p>In this post we got started with building a component using the AntBlazor library. In the next post, I continue the UI layout with AntBlazor.</p>
<p><a target="_blank" href="https://lukeparker.dev/blog/building-a-beautiful-okr-with-antblazor/2">← Part 2</a></p>
]]></content:encoded></item><item><title><![CDATA[Building a Beautiful OKR with AntBlazor - Part 2]]></title><description><![CDATA[Planning the Backend
There is one objective with many key results. I added some properties that I think will be useful. These may change as we figure out the UI/UX a bit more.
Some feature ideas:

Objectives should be toggleable if they are active or...]]></description><link>https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-2</link><guid isPermaLink="true">https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-2</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 04 Feb 2021 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-planning-the-backend">Planning the Backend</h2>
<p>There is one objective with many key results. I added some properties that I think will be useful. These may change as we figure out the UI/UX a bit more.</p>
<p>Some feature ideas:</p>
<ul>
<li>Objectives should be toggleable if they are active or not</li>
<li>Objectives should have a start and end date</li>
<li>Objectives should have a brief title, and a longer description</li>
<li>Key Results should have a brief title, and a longer description</li>
<li>Key Results should be toggleable if they are active or not</li>
<li>Key Results should have a completion percent 0%-100%</li>
<li>Key Results should have a priority (higher the number the more important)</li>
</ul>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Objective</span>
{
    <span class="hljs-keyword">public</span> Guid Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Title { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Description { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> DateTime DueDate { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> DateTime StartDate { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> Active { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">KeyResult</span>
{
    <span class="hljs-keyword">public</span> Guid Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> Guid ObjectiveId { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Title { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Description { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> Active { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> Completion { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Priority { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p>That's it for the core models! Lets add it to the project...</p>
<h2 id="heading-coding-it">Coding it</h2>
<p>In the <code>OKR.Core</code> project, create a folder called <code>Models</code> - you guessed it, for our domain models. Now add the two models from above in files: <code>Objective.cs</code> and <code>KeyResult.cs</code></p>
<h3 id="heading-repositories">Repositories</h3>
<p>Now we have the core models, we need to build an interface that abstracts data access. Lets build two repositories for the Objectives and Key Results.</p>
<p>Create another folder in <code>OKR.Core</code> called <code>Repositories</code>.</p>
<p>Data access code, especially in repositories can get pretty repetitive, so lets abstract it to a generic repository:</p>
<p>IGenericRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> System.Collections.Generic;
<span class="hljs-keyword">using</span> System.Threading.Tasks;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">OKR.Core.Repositories</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IGenericRepository</span>&lt;<span class="hljs-title">T</span>&gt;
    {
        <span class="hljs-function"><span class="hljs-keyword">public</span> Task <span class="hljs-title">AddAsync</span>(<span class="hljs-params">T model</span>)</span>;
        <span class="hljs-function"><span class="hljs-keyword">public</span> Task <span class="hljs-title">UpdateAsync</span>(<span class="hljs-params">T model</span>)</span>;
        <span class="hljs-function"><span class="hljs-keyword">public</span> Task <span class="hljs-title">DeleteAsync</span>(<span class="hljs-params">T model</span>)</span>;

        <span class="hljs-keyword">public</span> Task&lt;IReadOnlyList&lt;T&gt;&gt; GetAllAsync();
        <span class="hljs-keyword">public</span> Task&lt;IReadOnlyList&lt;T&gt;&gt; GetPagedAsync(<span class="hljs-keyword">int</span> page = <span class="hljs-number">0</span>);
    }
}
</code></pre>
<p>This code provides the simple CRUD operations on any model <code>T</code></p>
<p>Now lets build the specific repositories for OKRs:</p>
<p>IObjectiveRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> System;
<span class="hljs-keyword">using</span> System.Collections.Generic;
<span class="hljs-keyword">using</span> System.Threading.Tasks;
<span class="hljs-keyword">using</span> OKR.Core.Models;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">OKR.Core.Repositories</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IObjectiveRepository</span> : <span class="hljs-title">IGenericRepository</span>&lt;<span class="hljs-title">Objective</span>&gt;
    {
        Task&lt;IReadOnlyList&lt;Objective&gt;&gt; GetAllActiveAsync(<span class="hljs-keyword">bool</span> active = <span class="hljs-literal">true</span>);
        Task&lt;IReadOnlyList&lt;Objective&gt;&gt; GetAllWithinDueDateAsync();
        <span class="hljs-function">Task&lt;Objective&gt; <span class="hljs-title">GetByIdAsync</span>(<span class="hljs-params">Guid id</span>)</span>;
    }
}
</code></pre>
<p>Note the added functions that are specific to <code>Objective</code> properties</p>
<p>IKeyResultRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> System;
<span class="hljs-keyword">using</span> System.Collections.Generic;
<span class="hljs-keyword">using</span> System.Threading.Tasks;
<span class="hljs-keyword">using</span> OKR.Core.Models;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">OKR.Core.Repositories</span>
{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IKeyResultRepository</span> : <span class="hljs-title">IGenericRepository</span>&lt;<span class="hljs-title">KeyResult</span>&gt;
    {
        Task&lt;IReadOnlyList&lt;KeyResult&gt;&gt; GetAllByObjectiveAsync(Guid objectiveId);
    }
}
</code></pre>
<p>Pretty simple here, I don't think we need to provide any filtering on the data access level here, since there won't be any ridiculous number of key results inside an objective</p>
<h2 id="heading-mocking-the-repositories">Mocking the Repositories</h2>
<p>Before we get started with the frontend we have to mock the repositories to get some 'real' data.</p>
<p>Lets create a new class library project to hold the mock repositories called <code>OKR.Core.Mock</code>. Add a project reference to <code>OKR.Core</code>. Create a folder called <code>Repositories</code> and create the files: <code>MockObjectiveRepository.cs</code> and <code>MockKeyResultRepository.cs</code> - implementing the underlying interface. You should be able to autogenerate the functions like so:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MockObjectiveRepository</span> : <span class="hljs-title">IObjectiveRepository</span>
    {
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">AddAsync</span>(<span class="hljs-params">Objective model</span>)</span>
        {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();
        }

        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">UpdateAsync</span>(<span class="hljs-params">Objective model</span>)</span>
        {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();
        }

        ...
</code></pre>
<p>At this point, lets just make a couple functions return mock data, the rest can throw exceptions:</p>
<p>MockKeyResultRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IReadOnlyList&lt;KeyResult&gt;&gt; GetAllByObjectiveAsync(Guid objectiveId) =&gt; <span class="hljs-keyword">new</span>[]
{
    <span class="hljs-keyword">new</span> KeyResult
    {
        Id = Guid.NewGuid(),
        Active = <span class="hljs-literal">true</span>,
        Completion = <span class="hljs-number">0</span>,
        Description = <span class="hljs-string">"Do something..."</span>,
        Title = <span class="hljs-string">"Something"</span>,
        Priority = <span class="hljs-number">1</span>,
        ObjectiveId = objectiveId
    },
    <span class="hljs-keyword">new</span> KeyResult
    {
        Id = Guid.NewGuid(),
        Active = <span class="hljs-literal">true</span>,
        Completion = (<span class="hljs-keyword">decimal</span>) <span class="hljs-number">0.35</span>,
        Description = <span class="hljs-string">"Do this..."</span>,
        Title = <span class="hljs-string">"This"</span>,
        Priority = <span class="hljs-number">4</span>,
        ObjectiveId = objectiveId
    }
    ,
    <span class="hljs-keyword">new</span> KeyResult
    {
        Id = Guid.NewGuid(),
        Active = <span class="hljs-literal">true</span>,
        Completion = (<span class="hljs-keyword">decimal</span>) <span class="hljs-number">0.80</span>,
        Description = <span class="hljs-string">"Do that..."</span>,
        Title = <span class="hljs-string">"This"</span>,
        Priority = <span class="hljs-number">10</span>,
        ObjectiveId = objectiveId
    }
};
</code></pre>
<p>MockObjectiveRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IReadOnlyList&lt;Objective&gt;&gt; GetAllAsync() =&gt; <span class="hljs-keyword">new</span>[]
{
    <span class="hljs-keyword">new</span> Objective
    {
        Active = <span class="hljs-literal">true</span>,
        Title = <span class="hljs-string">"Q1 Growth"</span>,
        Description = <span class="hljs-string">"Grow in Q1 by doing some productive things."</span>,
        Id = Guid.NewGuid(),
        StartDate = DateTime.Now.Subtract(TimeSpan.FromDays(<span class="hljs-number">4</span>)),
        DueDate = DateTime.Now.AddDays(<span class="hljs-number">5</span>)
    },
    <span class="hljs-keyword">new</span> Objective
    {
        Active = <span class="hljs-literal">true</span>,
        Title = <span class="hljs-string">"Lorem Ipsum"</span>,
        Description = <span class="hljs-string">"A sentence describing what this is"</span>,
        Id = Guid.NewGuid(),
        StartDate = DateTime.Now.Subtract(TimeSpan.FromDays(<span class="hljs-number">4</span>)),
        DueDate = DateTime.Now.AddDays(<span class="hljs-number">5</span>)
    },
    <span class="hljs-keyword">new</span> Objective
    {
        Active = <span class="hljs-literal">true</span>,
        Title = <span class="hljs-string">"Q2 Growth"</span>,
        Description = <span class="hljs-string">"Grow in Q2 by doing some productive things."</span>,
        Id = Guid.NewGuid(),
        StartDate = DateTime.Now.Subtract(TimeSpan.FromDays(<span class="hljs-number">20</span>)),
        DueDate = DateTime.Now.AddDays(<span class="hljs-number">5</span>)
    },
    <span class="hljs-keyword">new</span> Objective
    {
        Active = <span class="hljs-literal">true</span>,
        Title = <span class="hljs-string">"Q3 Growth"</span>,
        Description = <span class="hljs-string">"Grow in Q3 by doing some productive things."</span>,
        Id = Guid.NewGuid(),
        StartDate = DateTime.Now.Subtract(TimeSpan.FromDays(<span class="hljs-number">50</span>)),
        DueDate = DateTime.Now.AddDays(<span class="hljs-number">100</span>)
    },
    <span class="hljs-keyword">new</span> Objective
    {
        Active = <span class="hljs-literal">false</span>,
        Title = <span class="hljs-string">"Q4 Growth"</span>,
        Description = <span class="hljs-string">"Grow in Q4 by doing some productive things."</span>,
        Id = Guid.NewGuid(),
        StartDate = DateTime.Now.AddDays(<span class="hljs-number">100</span>),
        DueDate = DateTime.Now.AddDays(<span class="hljs-number">200</span>)
    },
};
</code></pre>
<p>Now we have some mock repositories return <em>some</em> data.</p>
<hr />
<p>In the next post, I begin the UI with AntBlazor, using the mock data.</p>
<p><a target="_blank" href="https://lukeparker.dev/blog/building-a-beautiful-okr-with-antblazor/1">← Part 1</a> | <a target="_blank" href="https://lukeparker.dev/blog/building-a-beautiful-okr-with-antblazor/3">Part 3 →</a></p>
]]></content:encoded></item><item><title><![CDATA[Building a Beautiful OKR with AntBlazor - Part 1]]></title><description><![CDATA[Boilerplate
GitHub

Create a repository called OKR, and initialize it with only a README.md file
Open GitHub Desktop, and clone the repository to your machine

Folder Structure
The physical folder structure will be:
OKR
│   README.md
│   Docker Relat...]]></description><link>https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-1</link><guid isPermaLink="true">https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-1</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Tue, 02 Feb 2021 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-boilerplate">Boilerplate</h2>
<h3 id="heading-github">GitHub</h3>
<ol>
<li>Create a repository called <code>OKR</code>, and initialize it with only a README.md file</li>
<li>Open GitHub Desktop, and clone the repository to your machine</li>
</ol>
<h3 id="heading-folder-structure">Folder Structure</h3>
<p>The physical folder structure will be:</p>
<pre><code>OKR
│   README.md
│   Docker Related files
|   .sln    
│   ...
|
└───src
    │   
    └─── ... (Projects e.g. <span class="hljs-string">'OKR.Web'</span>)
        │   .csproj
        │   Dockerfile
        │   ...
</code></pre><h3 id="heading-setup-solution-projects">Setup Solution + Projects</h3>
<ol>
<li>Open the folder in the file explorer, and <code>Shift + Right Click -&gt; Open PowerShell window here</code>. Type <code>dotnet new gitignore</code> to generate a .gitignore file. Feel free to make this file the first git commit</li>
<li>Create a new empty solution (<code>Blank Solution</code> in VS, <code>Empty Solution</code> in Rider) and call it <code>OKR</code></li>
<li>Create a solution folder <code>src/</code> - this is not a physical folder, its part of the solution</li>
<li>Create a solution folder <code>Solution Items</code>, and add the existing items, README.md and .gitignore. In the future the docker files will be added here too</li>
<li>Add 4 projects to the <code>src/</code> solution folder, and the <code>src/</code> physical folder path - these are following the DDD principals<ul>
<li><code>OKR.Web</code> <strong>Blazor Server</strong> - here is where the Blazor frontend code will be, separate from business logic + data access layers</li>
<li><code>OKR.Application</code> <strong>Class Library</strong> - here is the business logic layer with references to repository interfaces from the <code>OKR.Core</code> layer to abstract data access away from business logic</li>
<li><code>OKR.Core</code> <strong>Class Library</strong> - here is the core domain models, and repository interfaces</li>
<li><code>OKR.Infrastructure</code> <strong>Class Library</strong> - here are the implementations of repository interfaces using Marten</li>
</ul>
</li>
</ol>
<h2 id="heading-devops">DevOps</h2>
<p>Its always nice to have DevOps from day 0 of your project, so lets add it!</p>
<h3 id="heading-github-ci">GitHub CI</h3>
<p>To setup the dotnet build + test action, go to the <code>GitHub Repository -&gt; Actions -&gt; .NET -&gt; Set up this workflow</code>. The template file works without any changes, because our .sln is in the root directory - so click <code>Start commit -&gt; Commit directly to the main branch</code>, add a commit message like <code>Add dotnet CI</code>, and finally <code>Commit new file</code></p>
<h3 id="heading-docker">Docker</h3>
<p>First we need a Dockerfile to create an image for the Blazor site. Visual Studio automatically generates Dockerfiles - right click the OKR.Web project, <code>Add -&gt; Docker Support -&gt; Linux -&gt; Ok</code>. That's all it takes! When projects get dependencies make sure to run this again (like when you reference projects)</p>
<h3 id="heading-docker-compose">Docker Compose</h3>
<p>Now we need to orchestrate the config for the Blazor site, and include the postgres database all in one.</p>
<p>Create a blank file called <code>docker-compose.yml</code> and add it to the <code>Solution Items/</code> folder in the .sln.</p>
<p>Fill it out with the following:</p>
<pre><code class="lang-yml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3.8"</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">frontend:</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">src/OKR.Web/Dockerfile</span>
      <span class="hljs-attr">context:</span> <span class="hljs-string">.</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">okr-frontend</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">OkrFrontend</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ASPNETCORE_ENVIRONMENT=Production</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ASPNETCORE_ConnectionStrings__Marten=User</span> <span class="hljs-string">ID</span> <span class="hljs-string">=</span> <span class="hljs-string">okr;Password=o32342134k4r%Y#%Y345yRasdf;Server=postgres;Port=5432;Database=okr_db;Integrated</span> <span class="hljs-string">Security=true;Pooling=true</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./config/appsettings.json:/app/appsettings.json</span>
    <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">"9595:80"</span>
  <span class="hljs-attr">postgres:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">OkrPostgres</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_USER=okr</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_PASSWORD=o32342134k4r%Y#%Y345yRasdf</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_DB=okr_db</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./data:/var/lib/postgresql/data</span>
</code></pre>
<p>Note the two services, the frontend and database. The secrets for the database are hardcoded - this is fine since the database is not exposed to the internet, but for added security feel free to make this in a config file.</p>
<p>The frontend is exposed to the VPS's port 9595, proxying the request to the containers port 80.</p>
<p>To start the whole system you can run <code>docker-compose up -d --build</code> (<code>-d</code> detached mode, <code>--build</code> builds the Dockerfile)</p>
<p>Ensure on your VPS you have installed Git, Docker and Docker Compose.</p>
<h3 id="heading-domain-reverse-proxy">Domain + Reverse Proxy</h3>
<h4 id="heading-cloudflare">Cloudflare</h4>
<p>This site will run on a subdomain <code>okr.lukeparker.dev</code>, so I will walk through that. (If you want to point it to a root domain, like <code>lukeparker.dev</code> use an <code>A</code> record pointing to your VPS IP address instead)</p>
<p>Since I have a record for <code>lukeparker.dev</code> to point to my VPS, I just need to add a CNAME record to proxy <code>okr.lukeparker.dev</code> to the same IP as <code>lukeparker.dev</code>.</p>
<p>Open up the Cloudflare dashboard and goto the site (in my case <code>lukeparker.dev</code>), the click the DNS page. Click <code>Add record</code>:</p>
<ul>
<li>Change Type to: <code>CNAME</code></li>
<li>Change Name to: <code>okr</code></li>
<li>Change Content to: <code>@</code> (<code>@</code> will resolve to the base domain - <code>lukeparker.dev</code>)</li>
</ul>
<p>Then click save.</p>
<p>This may take a minute or two to update in Cloudflare's network, but it is pretty fast.</p>
<h4 id="heading-nginx-on-vps">NGINX on VPS</h4>
<p>Ok so now we have requests incoming, we need to proxy <code>okr.lukeparker.dev -&gt; localhost:9595</code></p>
<p>I have a global NGINX docker-compose repository you can reference that I use for my VPS and sites: <a target="_blank" href="https://github.com/Hona/azetio-nginx">azetio-nginx</a></p>
<p>Before we begin you need to install the Cloudflare Origin Certificate on your server, so follow <a target="_blank" href="https://support.cloudflare.com/hc/en-us/articles/115000479507-Managing-Cloudflare-Origin-CA-certificates">this guide</a> - make sure to note where and what the certificates are called. If you are running your global NGINX server in Docker, make sure to mount the certificate files. <a target="_blank" href="https://blog.cloudflare.com/cloudflare-ca-encryption-origin/">Read more here</a></p>
<p>If you used my azetio-nginx, then add a new file in nginx/sites-enabled/ called <code>lukeparker.dev.okr.conf</code> and fill it out with the following (renamed to match your domain):</p>
<pre><code class="lang-nginx"><span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;
    <span class="hljs-attribute">server_name</span> okr.lukeparker.dev www.okr.lukeparker.dev;

    <span class="hljs-attribute">return</span> <span class="hljs-number">301</span> https://okr.lukeparker.dev<span class="hljs-variable">$request_uri</span>;
}

<span class="hljs-section">server</span> {
    <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl;
    <span class="hljs-attribute">server_name</span> okr.lukeparker.dev www.okr.lukeparker.dev;

    <span class="hljs-attribute">ssl_certificate</span> /etc/ssl/certs/lukeparker.pem;
    <span class="hljs-attribute">ssl_certificate_key</span> /etc/ssl/private/lukeparker.pem;
    <span class="hljs-attribute">ssl_client_certificate</span> /etc/ssl/certs/origin-pull-ca.pem;
    <span class="hljs-attribute">ssl_verify_client</span> <span class="hljs-literal">on</span>;

    <span class="hljs-attribute">ssl_session_cache</span> builtin:<span class="hljs-number">1000</span> shared:SSL:<span class="hljs-number">10m</span>;
    <span class="hljs-attribute">ssl_protocols</span> TLSv1 TLSv1.<span class="hljs-number">1</span> TLSv1.<span class="hljs-number">2</span>;
    <span class="hljs-attribute">ssl_ciphers</span> HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
    <span class="hljs-attribute">ssl_prefer_server_ciphers</span> <span class="hljs-literal">on</span>;

    <span class="hljs-attribute">location</span> / {
        <span class="hljs-attribute">proxy_pass</span> http://localhost:9595;
        <span class="hljs-attribute">proxy_http_version</span> <span class="hljs-number">1</span>.<span class="hljs-number">1</span>;
        <span class="hljs-attribute">proxy_set_header</span> Upgrade <span class="hljs-variable">$http_upgrade</span>;
        <span class="hljs-attribute">proxy_set_header</span> Connection keep-alive;
        <span class="hljs-attribute">proxy_set_header</span> Host <span class="hljs-variable">$host</span>;
        <span class="hljs-attribute">proxy_cache_bypass</span> <span class="hljs-variable">$http_upgrade</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-For <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
        <span class="hljs-attribute">proxy_set_header</span> X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
        <span class="hljs-attribute">proxy_read_timeout</span> <span class="hljs-number">3600</span>;
    }
}
</code></pre>
<p>This configuration will automatically redirect http -&gt; https, and proxy requests to the 9595 port, passing forwarded headers.</p>
<p>Note that if you aren't using my nginx config: add this to the <code>http { ... }</code> clause:</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">map</span> <span class="hljs-variable">$http_upgrade</span> <span class="hljs-variable">$connection_upgrade</span> {
        <span class="hljs-attribute">default</span> Upgrade;
        '' close;
    }
</code></pre>
<p>Make sure to read <a target="_blank" href="https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-5.0">Host ASP.NET Core on Linux with Nginx</a> to understand the config.</p>
<p>With all this setup, make sure to restart the azetio-nginx config with <code>docker-compose up -d --force-recreate</code></p>
<p>On the VPS, we need to first clone the git repository, so run <code>git clone git@github.com:Hona/OKR</code> (Replace with your username + repository)</p>
<p>Navigate to the repo: <code>cd OKR</code></p>
<p>Make sure to create the config directory and an empty appsettings.json file:</p>
<pre><code class="lang-shell">mkdir config
echo "{ }" &gt; "config/appsettings.json"
</code></pre>
<p>Then start the OKR project with 
<code>docker-compose up -d --build</code></p>
<p>If all goes well, navigate to the domain you setup, and you should see the default Blazor page, if you do, everything is setup perfectly!</p>
<hr />
<p>In the next post, I will plan and setup the core models, repository interfaces and mock repositories.</p>
<p><a target="_blank" href="https://lukeparker.dev/blog/building-a-beautiful-okr-with-antblazor/0">← Part 0</a> | <a target="_blank" href="https://lukeparker.dev/blog/building-a-beautiful-okr-with-antblazor/2">Part 2 →</a></p>
]]></content:encoded></item><item><title><![CDATA[Building a Beautiful OKR with AntBlazor - Part 0]]></title><description><![CDATA[Introduction
Why?
There are many boilerplate Blazor examples everywhere online, showing the basics, and some more intricate parts. However, I am yet to find a full blog series showing end-to-end, how to make a beautiful UI and clean coded backend 're...]]></description><link>https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-0</link><guid isPermaLink="true">https://lukeparker.dev/building-a-beautiful-okr-with-antblazor-part-0</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Mon, 01 Feb 2021 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<h3 id="heading-why">Why?</h3>
<p>There are many boilerplate Blazor examples everywhere online, showing the basics, and some more intricate parts. However, I am yet to find a full blog series showing end-to-end, how to make a beautiful UI and clean coded backend 'real world' website with Blazor</p>
<h3 id="heading-the-idea">The Idea</h3>
<p>For this series I decided that a reasonably simple but more complex than the typical TODO app, is an OKR website. If you aren't familiar with what an OKR tracker is, it stands for <code>objectives and key results</code>. What this means is there are larger objectives, that can be tracked by specific key results.</p>
<p>An example might be:</p>
<p>O: Become the top retailer in your state</p>
<p>KR:</p>
<ul>
<li>Increase sales by 45%</li>
<li>Decrease monthly costs by $2000</li>
</ul>
<p>From a developer perspective this is a simple enough app, you have <strong>one</strong> objective to <strong>many</strong> key results. This means the focus of this series will be to dive into UI and hooking it up with Blazor.</p>
<h3 id="heading-series-roadmap">Series Roadmap</h3>
<ol>
<li>Scaffold the project boilerplate using DDD (domain driven design), and full DevOps (GitHub CI, Docker + docker-compose, NGINX domain-&gt;docker setup)</li>
<li>Plan the backend, and setup the core models and repository interfaces</li>
<li>Build mock repositories with test data, to be able to build the UI properly, once. Build the main UI sections</li>
<li>Build the backend repositories and database setup using Marten DB as a document store</li>
<li>Build an analytics dashboard using data from all OKRs</li>
</ol>
<h4 id="heading-potential-extensions">Potential Extensions</h4>
<ul>
<li>Add authentication, authorization, users/groups</li>
<li>Add an API to expose data for 3rd party use</li>
</ul>
<h2 id="heading-the-tech-stack">The Tech Stack</h2>
<p>Domain Registrar: <strong>Cloudflare</strong></p>
<p>DDoS Protection, Nameservers, DNS: <strong>Cloudflare</strong></p>
<p>Server Hosting: <strong>Vultr</strong> VPS</p>
<p>Reverse Proxy (differentiate between domains): <strong>NGINX</strong></p>
<p>Containers: <strong>Docker</strong> and <strong>docker-compose</strong></p>
<p>Website: <strong>ASP.NET Core Blazor + C#</strong></p>
<p>Database: <strong>Postgres</strong></p>
<h2 id="heading-ui">UI</h2>
<p>There are many UI frameworks for Blazor, but many are hard to use, require knowledge from React and more. The only UI framework I've found for Blazor that makes sense, and is easy to use - is <a target="_blank" href="https://antblazor.com/">AntBlazor</a>. AntBlazor is a Blazor component library for <a target="_blank" href="https://ant.design/">Ant Design</a>.</p>
<p>This library supports many components, and its component overview can be found on the linked site. I've personally used AntBlazor for many of my personal projects, and for paid projects - in production. It is still a somewhat young library compared to React or Angular UI libraries, but Blazor is the new tech on the block.</p>
<p>In this blog series I am going to show you how easy it is to build a beautiful frontend; this is especially useful if you are primarily a backend developer and don't have/want to put in the time for the frontend.</p>
<hr />
<p>In the next post, I will setup the project boilerplate and full DevOps.</p>
<p><a target="_blank" href="https://lukeparker.dev/blog/building-a-beautiful-okr-with-antblazor/1">Part 1 →</a></p>
]]></content:encoded></item><item><title><![CDATA[Why I switched to Rider from Visual Studio for C# Development]]></title><description><![CDATA[Context
I have personally been using Visual Studio since I started learning to code, all the way back in 2010 using VB.NET. Over that time I have grown more comfortable with using the IDE. Made by Microsoft, and considered the flagship way to develop...]]></description><link>https://lukeparker.dev/why-i-switched-to-rider-from-visual-studio-for-c-development</link><guid isPermaLink="true">https://lukeparker.dev/why-i-switched-to-rider-from-visual-studio-for-c-development</guid><dc:creator><![CDATA[Luke Parker]]></dc:creator><pubDate>Thu, 07 Jan 2021 00:00:00 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-context">Context</h2>
<p>I have personally been using Visual Studio since I started learning to code, all the way back in 2010 using VB.NET. Over that time I have grown more comfortable with using the IDE. Made by Microsoft, and considered the flagship way to develop for C#, why would I want to switch away from it?</p>
<h2 id="heading-visual-studios-shortcomings">Visual Studio's Shortcomings</h2>
<ul>
<li>Very laggy - Visual Studio as a piece of software is an age-old behemoth, an unavoidable recipe for lag, and severe slow downs. This becomes evident once you start loading projects larger than a simple proof of concept. More recently, I've noticed these issues becoming more extreme.</li>
<li>Many, many slight issues and bugs. Little annoying issues that aren't code destroying, but enough to slow down your productivity, or break you out of your flow state. An example of one of the bugs is an unlimited loading time when adding a reference to another project through a <em>'quick' action</em>.</li>
<li>The <em>almost</em> necessity for ReSharper, and other VS Extensions.</li>
</ul>
<p>Although my list isn't that large, they all contribute to being less productive, and in ReSharper's case, already reliant on JetBrain's products.</p>
<h2 id="heading-making-the-switch">Making the Switch</h2>
<blockquote>
<p>It should be mentioned that Rider is a paid product, however you can get a free for personal use license as a student, or even as an open source maintainer.</p>
</blockquote>
<p>Downloading Rider was a simple process through their website, and the install process was seamless.</p>
<p>A nice feature that comes with Rider (and ReSharper for VS), was the keymap options, which allows you to instantly get a familiar experience, no matter what IDE you come from.</p>
<h2 id="heading-initial-impressions">Initial Impressions</h2>
<p>I immediately loaded up a reasonably large sized project (10+ projects), to see how Rider holds up. Wow! I was blown away... I've never seen such a smooth experience; loading a solution, indexing files/symbols, code inspection + analysis were all amazingly smooth! The background tasks that usually run in the 'background' of Visual Studio - often causing the syntax highlighting to break, IntelliSense to freeze, and sometimes the window itself to stop responding - went by unnoticed in Rider. You can start coding right away as these tasks fire off in the background, so if you boot up Rider with that bug fix that suddenly came to you in the shower, no need to wait upwards of several minutes to open the IDE. </p>
<h2 id="heading-riders-downfalls">Rider's Downfalls</h2>
<p>Even though Rider is a great product, and I will not be going back to Visual Studio anytime soon, it still comes with its downsides.</p>
<ul>
<li>No way to auto-generate a Dockerfile - in VS you can <code>Right Click -&gt; Add -&gt; Docker Support</code> to instantly dockerize your project, however this option does not exist in Rider. This is the only reason that I open Visual Studio anymore, to simply generate Dockerfiles.</li>
<li>Some code formatting issues, upon saving rider keeps pointless whitespace, so if you have accidental spaces after a <code>;</code> or <code>}</code>, it will be there, luckily git diffs show this so I can manually delete them before commiting, but this is a missed feature from VS. Autoformatting does fix this, but really, it should be done on save.</li>
<li>You cannot edit a <code>.csproj</code> file without unloading the project first, making the change, and reloading it. Coming from VS, you know that you can simply click the project and edit the file without issue.</li>
</ul>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Rider is a great product, and the simple fact that it is <strong>significantly more performant</strong>, and less bug-filled than Visual Studio is enough for me to make the permanent switch. As I mentioned above, some small issues and missing features annoy me from time to time, but is nothing compared to freezes, lag and bugs from Visual Studio. </p>
<p>I'd recommend that everyone gives Rider a go for at least a week (there's a 30 day free trial), because trust me, once you switch, you cannot go back!</p>
]]></content:encoded></item></channel></rss>