Los Techies http://feed.informer.com/digests/ZWDBOR7GBI/feeder Los Techies Respective post owners and feed distributors Thu, 07 Jul 2022 16:23:30 +0000 Feed Informer http://feed.informer.com/ Big Design Up Front Is Responsible Again https://lostechies.com/erichexter/2026/07/31/big-design-up-front-is-responsible-again/ Los Techies urn:uuid:74d40f4e-88ca-3c76-7fb9-efc1313f6fed Fri, 31 Jul 2026 09:00:00 +0000 Agile taught us to avoid big design up front. That was the right call — for the constraints we had. Those constraints are gone, and I think the advice inverted. <p>Agile taught us to avoid big design up front. That was the right call — for the constraints we had. Those constraints are gone, and I think the advice inverted.</p> <p>Here’s why we avoided it. Two things were slow and expensive: <strong>writing the spec, and building the software.</strong> Both were done by hand. So a detailed upfront design was a large bet you couldn’t easily change, written before you knew if it was right. The sane response was to write less of it, ship a thin slice, and learn by building the wrong thing first. For twenty years that was the responsible move. I’ve shipped plenty of thin slices that discovered the wrong product — expensively — and called it agility.</p> <p>AI deleted both of those costs.</p> <p>Last week I designed a whole application before writing a line of it. Not a napkin sketch — a complete, traceable spec: stakeholder needs, system requirements, architecture with sequence diagrams, down to component-level tickets. Then it was rendered into a <strong>narrated video walkthrough with real UI screens</strong>, and I handed that to the customer to review. Pause, draw on a frame, leave a voice note.</p> <p><img src="/content/erichexter/uploads/2026/07/design-review-frame.png" alt="A design review, rendered: a real UI screen with narration, reviewed before any code was written" style="max-width:100%" /></p> <p>Writing that spec used to take weeks of typing. It took hours. Building a throwaway version just to get real feedback used to be the only option. Now I get feedback on the design itself, at full fidelity, before building anything.</p> <p>The part that matters: <strong>this is not waterfall.</strong> Waterfall failed because the big upfront design was unvalidated <em>and</em> expensive to change — by the time reality showed up, you were committed. Flip both of those. The design is now cheap to produce, cheap to change, and cheap to validate: render a new video, get feedback, re-render. That is big design up front <em>with</em> a tight feedback loop — the thing waterfall couldn’t afford and agile gave up on.</p> <p>Put it in a grid. Waterfall: expensive to produce, expensive to change. Agile: so skip the design. What we have now: cheap to produce, cheap to change, cheap to validate. Different quadrant. Different rules.</p> <p><img src="/content/erichexter/uploads/2026/07/design-screen-set.png" alt="A full set of designed screens generated for review before implementation" style="max-width:100%" /></p> <p>So where did the bottleneck go? It was never typing. It was <em>typing speed</em> — for specs and for code — that forced us to go slow and iterate. The model writes specs at the speed of light and builds at the speed of light. The only scarce thing left is <strong>judgment</strong>: deciding what’s right, and getting the humans aligned before you commit.</p> <p>Spend your time there. Do the design work you’ve been skipping since 2005 — you can finally afford it. Refine it. Review it properly, with people who aren’t going to read a forty-page doc but will watch a five-minute video and tell you it’s wrong. Then let the model build it.</p> <p>Concretely, the loop I’m running now:</p> <ol> <li>A living spec that deepens by level — stakeholder need, system, architecture, components — each traceable to the next.</li> <li>Every level rendered into a narrated review video, approved before the design goes deeper.</li> <li>The final design compiles to tickets.</li> <li>The model implements; you verify against the spec you already agreed on.</li> </ol> <p>The responsible move in 2026 isn’t “move fast and iterate.” It’s <strong>get it right, then let the machine move fast.</strong></p> Vertical Slice Architecture Webinar https://www.jimmybogard.com/vertical-slice-architecture-webinar/ Jimmy Bogard urn:uuid:c053f869-7a63-d2af-0910-fa0f86db5508 Thu, 23 Jul 2026 18:57:00 +0000 <p>Next month I&apos;m hosting a free webinar on Vertical Slice Architecture, DDD, and how they fit in with AI-assisted development. I, like a lot of developers, have embraced AI assistance for designing and building systems. While these tools can accelerate the <strong>creation</strong> of code, they don&apos;</p> <p>Next month I&apos;m hosting a free webinar on Vertical Slice Architecture, DDD, and how they fit in with AI-assisted development. I, like a lot of developers, have embraced AI assistance for designing and building systems. While these tools can accelerate the <strong>creation</strong> of code, they don&apos;t necessarily create <strong>malleable or maintainable</strong> code. This has real-world impacts on the systems we build - burning tokens on long review cycles, introducing bugs, or simply complexity, rather than value-add features. In this webinar, I&apos;ll show how VSA can shorten those cycles by reducing side-effects and coupling in your systems.</p><p>The webinar is next month (August 20th) and you can register for free here:</p><p><a href="https://codeartify.com/en/public-video-sessions/vertical-slice-architecture-effective-guardrails-for-ai-development?ref=jimmybogard.com" rel="noreferrer">Vertical Slice Architecture: Effective Guardrails for AI Development</a></p><p>Hope to see you there!</p> AutoMapper 16.2.0 and MediatR 14.2.0 Released https://www.jimmybogard.com/automapper-16-2-0-and-mediatr-14-2-0-released/ Jimmy Bogard urn:uuid:4ceac967-4343-470a-40ee-3a3b8be77cd6 Thu, 02 Jul 2026 14:09:17 +0000 <p>Today we released the 16.2.0 version of AutoMapper and 14.2.0 version of MediatR:</p><ul><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.2.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper Release Notes</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v14.2.0?ref=jimmybogard.com" rel="noreferrer">MediatR Release Notes</a></li></ul><p>This release is a bit more enterprise-focused, with extensions for setting the license keys via environment variables, fixing some threading issues around license key validation, and</p> <p>Today we released the 16.2.0 version of AutoMapper and 14.2.0 version of MediatR:</p><ul><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.2.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper Release Notes</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v14.2.0?ref=jimmybogard.com" rel="noreferrer">MediatR Release Notes</a></li></ul><p>This release is a bit more enterprise-focused, with extensions for setting the license keys via environment variables, fixing some threading issues around license key validation, and including more security-related items on releases (SBOMs, etc.)</p><p>These last few releases have been targeting items more or less required for enterprises consuming commercial packages. In the next release, we&apos;ll be more focused on adding features and fixing historical bugs.</p><ul><li><a href="https://www.nuget.org/packages/AutoMapper?ref=jimmybogard.com" rel="noreferrer">AutoMapper NuGet</a></li><li><a href="https://www.nuget.org/packages/MediatR?ref=jimmybogard.com" rel="noreferrer">MediatR NuGet</a></li></ul><p>Enjoy!</p> 23 Models, One Weekend, Final Picks https://lostechies.com/erichexter/2026/06/06/local-llm-bench-part-5-final-picks/ Los Techies urn:uuid:2bf1842f-151b-faf7-0ac0-3a39e4d4a7fd Sat, 06 Jun 2026 12:00:00 +0000 Part 5 of 5 in the Local LLM Bench series. <p><em>Part 5 of 5 in the <a href="/erichexter/2026/05/25/local-llm-bench-part-1-which-models-can-chat/">Local LLM Bench series</a>.</em></p> <p>The project started with ten models and two prompts. It ended with 23 models, a 13-point scoring harness, 3 Python agentic tasks, and more surprises per hour than I expected. This is the final leaderboard and the honest verdict.</p> <h2 id="expanding-to-23-models">Expanding to 23 Models</h2> <p>After the initial ten-model run, I pulled thirteen more based on a mix of research agent recommendations and community signals. The research was right about some things and wrong about others.</p> <p>It correctly killed two obvious traps. qwen2.5vl is a vision model, not a coder — the “vl” should have been the clue but I wanted confirmation. qwen3.5:27b is a thinking model that burns its token budget on internal reasoning before producing output; on 16GB VRAM with a standard context budget it hits the wall and times out on every agentic task. Both of those were correct calls.</p> <p>Then there was cogito:14b. The research said: skip it, superseded, runs 2-3 points behind qwen2.5. I almost listened. What actually happened when I ran cogito: 11-second code generation on the fizzbuzz task, 100/100 agentic score, both edit formats working cleanly. The research was wrong. Cogito turned out to be the fastest sweet-spot model I tested, and it passed tasks that models with higher single-shot scores failed entirely.</p> <p>Two tag hallucinations also surfaced during pulls. qwen3.5:9b doesn’t exist — only the 27B is available. qwen3-vl:8b doesn’t exist — only the 235B is available. The research had the right model families but invented specific version tags. The fix is always the same: check ollama.com/library before pulling. Don’t trust a model recommendation that includes a specific tag without verifying.</p> <h2 id="the-pi-harness-experiment">The Pi Harness Experiment</h2> <p>Alongside the expanded model pool, I tested a different agentic harness entirely. Pi is fundamentally different from aider: instead of receiving structured edit instructions, the model gets direct Bash tool access and can run <code class="language-plaintext highlighter-rouge">dotnet new</code>, <code class="language-plaintext highlighter-rouge">dotnet test</code>, and anything else itself. It operates as an autonomous loop rather than a guided editor.</p> <p>I ran devstral and qwen3-coder through pi on two tasks: fizzbuzz-plus and csv-parser. Both timed out at 1020 seconds. Not close calls — full exhaustion, zero useful output across both models and both tasks.</p> <p>The root cause is that pi is designed for models fine-tuned for tool-calling loops: NousResearch Hermes-class, OpenClaw, models explicitly trained to keep calling tools autonomously and self-terminate when done. Devstral and qwen3-coder via Ollama’s OpenAI-compat API don’t have that fine-tuning. They can use tools when prompted, but they don’t have the trained instinct to keep invoking tools in sequence until a test passes.</p> <p>The thing pi taught me even while failing: harness design is not neutral. An aider task prompt and a pi task prompt are different programs. The model receives different inputs, operates under different constraints, and requires different trained behaviors to succeed. A 100/100 aider score does not predict pi performance, and vice versa. If a Hermes-class model shows up in Ollama’s library with solid benchmark numbers, pi is worth revisiting. Until then, aider is the right tool for local 14-30B models.</p> <h2 id="the-scoring-expansion">The Scoring Expansion</h2> <p>The harness also grew. I extended the single-shot tests from 10 to 13 points by adding three new probes: a math word problem (3 apples at $0.50 plus 4 oranges at $0.75, reply with only the dollar amount), a JSON output test (return a JSON array of 3 programming languages, nothing else), and a sequence test (output 1 through 5, one per line, nothing else).</p> <p>These three tests turned out to be more discriminating than I expected. Ten of twenty-three models fail the $4.50 math test — not because they get the arithmetic wrong, but because they reason aloud about the problem instead of answering it. The sequence test catches models that follow instructions in general but can’t suppress the urge to add a brief explanation. The JSON test catches models that can’t stop themselves from wrapping output in markdown fences when explicitly told not to.</p> <p>None of these tests are hard. All of them reveal something real about how a model behaves when you need it to produce structured output on command.</p> <h2 id="three-new-python-agentic-tasks">Three New Python Agentic Tasks</h2> <p>The agentic suite expanded to include three Python tasks alongside the existing C# work. The tasks: a markdown-to-html converter (implement <code class="language-plaintext highlighter-rouge">md_to_html()</code>, 10 pytest tests covering headers, bold, italic, inline code, and links), a JSON validator (implement <code class="language-plaintext highlighter-rouge">validate(data, schema)</code> returning error strings, 9 pytest cases covering required fields, type checking, and enum validation), and a word-frequency counter (implement <code class="language-plaintext highlighter-rouge">top_words(text, n)</code> returning top-N tuples sorted by count descending then alphabetically, 8 pytest cases).</p> <p>I ran these on seven models: devstral, qwen3-coder, phi4, hermes3, qwen2.5-coder, mistral-small3.2, and codestral. The results reshuffled the leaderboard in ways the single-shot scores did not predict.</p> <h2 id="the-full-leaderboard">The Full Leaderboard</h2> <table> <thead> <tr> <th>Model</th> <th>Size</th> <th>SS /13</th> <th>Chat ms</th> <th>Code ms</th> <th>Agentic Best</th> <th>Agentic Pass%</th> </tr> </thead> <tbody> <tr> <td>gemma4:latest</td> <td>~12B</td> <td>12/13</td> <td>6,918</td> <td>603</td> <td>20/100</td> <td>0% (0/2)</td> </tr> <tr> <td>devstral:latest</td> <td>~24B</td> <td>11/13</td> <td>16,875</td> <td>3,246</td> <td>100/100</td> <td>83% (5/6)</td> </tr> <tr> <td>gemma4:26b</td> <td>26B</td> <td>11/13</td> <td>11,029</td> <td>3,255</td> <td>20/100</td> <td>0% (0/2)</td> </tr> <tr> <td>qwen3.5:27b</td> <td>27B</td> <td>11/13</td> <td>24,810</td> <td>7,222</td> <td>20/100</td> <td>0% (timeout)</td> </tr> <tr> <td>deepseek-r1:14b</td> <td>14B</td> <td>10/13</td> <td>6,286</td> <td>561</td> <td>—</td> <td>—</td> </tr> <tr> <td>glm-4.7-flash</td> <td>30B MoE</td> <td>10/13</td> <td>8,843</td> <td>2,531</td> <td>20/100</td> <td>0% (timeout)</td> </tr> <tr> <td>granite4:32b-a9b-h</td> <td>32B MoE</td> <td>10/13</td> <td>20,885</td> <td>3,125</td> <td>20/100</td> <td>0%</td> </tr> <tr> <td>qwen2.5:14b</td> <td>14B</td> <td>10/13</td> <td>6,221</td> <td>475</td> <td>10/100</td> <td>0%</td> </tr> <tr> <td>qwen2.5vl:7b</td> <td>7B</td> <td>10/13</td> <td>5,783</td> <td>863</td> <td>—</td> <td>—</td> </tr> <tr> <td>qwen3-coder:30b</td> <td>30B</td> <td>10/13</td> <td>9,948</td> <td>2,143</td> <td>100/100</td> <td>67% (4/6)</td> </tr> <tr> <td>qwen3:14b</td> <td>14B</td> <td>10/13</td> <td>3,876</td> <td>523</td> <td>20/100</td> <td>0%</td> </tr> <tr> <td>cogito:14b</td> <td>14B</td> <td>9/13</td> <td>6,447</td> <td>438</td> <td>—</td> <td>—</td> </tr> <tr> <td>hermes3:latest</td> <td>~8B</td> <td>9/13</td> <td>3,756</td> <td>280</td> <td>100/100</td> <td>40% (2/5)</td> </tr> <tr> <td>mistral-small3.2:24b</td> <td>24B</td> <td>9/13</td> <td>12,169</td> <td>3,228</td> <td>100/100</td> <td>100% (3/3)</td> </tr> <tr> <td>mistral:latest</td> <td>7B</td> <td>8/13</td> <td>3,335</td> <td>323</td> <td>20/100</td> <td>0%</td> </tr> <tr> <td>codestral:22b</td> <td>22B</td> <td>7/13</td> <td>17,182</td> <td>2,427</td> <td>100/100</td> <td>67% (2/3)</td> </tr> <tr> <td>deepseek-coder-v2:16b</td> <td>16B</td> <td>7/13</td> <td>6,516</td> <td>298</td> <td>—</td> <td>—</td> </tr> <tr> <td>llava:7b</td> <td>7B</td> <td>7/13</td> <td>4,045</td> <td>292</td> <td>—</td> <td>—</td> </tr> <tr> <td>magistral:24b</td> <td>24B</td> <td>7/13</td> <td>22,802</td> <td>11,568</td> <td>—</td> <td>—</td> </tr> <tr> <td>gpt-oss:20b</td> <td>20B</td> <td>6/13</td> <td>8,751</td> <td>8,915</td> <td>20/100</td> <td>0%</td> </tr> <tr> <td>phi4:14b</td> <td>14B</td> <td>6/13</td> <td>6,415</td> <td>466</td> <td>100/100</td> <td>50% (2/4)</td> </tr> <tr> <td>qwen2.5-coder:14b</td> <td>14B</td> <td>6/13</td> <td>5,989</td> <td>529</td> <td>100/100</td> <td>100% (4/4)</td> </tr> <tr> <td>qwen3:30b</td> <td>30B</td> <td>3/13</td> <td>14,866</td> <td>10,749</td> <td>—</td> <td>—</td> </tr> </tbody> </table> <p>qwen3.5:27b, gpt-oss:20b, and deepseek-r1:14b are thinking models — they burn context on internal reasoning before producing visible output. The scores reflect that.</p> <h2 id="the-surprising-results">The Surprising Results</h2> <p><strong>gemma4:latest.</strong> 12/13 single-shot, 603ms code generation, fastest chat in its size class. Zero percent agentic pass rate across every task it attempted. This is the sharpest split in the entire dataset. gemma4 is an excellent model for answering questions. It has no working mental model of “I am in a multi-turn loop writing files until tests pass.” Those are different capabilities. The single-shot tests reward the former. The agentic tasks require the latter. gemma4 nails one and is completely useless at the other.</p> <p><strong>mistral-small3.2:24b.</strong> I almost missed this one entirely. It had no agentic run history going into the final Python task batch — it just hadn’t come up in earlier experiments. When I finally ran it, it swept all three new Python tasks with 100/100 scores on first attempt, finishing each in 26 to 52 seconds. Nine out of 13 on single-shot. It had minimal community attention during the bench period. It turned out to be one of the two most reliable agentic performers I tested. The lesson here: community signal is a useful prior, not a substitute for running the test.</p> <p><strong>qwen2.5-coder:14b.</strong> 6/13 on single-shot. That score is a lie in the specific direction that matters most. The instruction-following tests fail consistently. The code generation test produces output that compiles but gets the wrong answer. On every agentic task I ran it on, it passed. Four for four, 100% pass rate. The single-shot harness penalizes its tendency to reason aloud before writing code. In an agentic loop, that verbosity doesn’t hurt — aider just waits for the edit block, and the edit block is correct. Single-shot actively mispredicts this model’s real-world utility.</p> <p><strong>hermes3:latest.</strong> 280ms code generation. The fastest model in the field by a significant margin, and at 4.7GB it’s the lightest serious option. 3,756ms average chat latency, also fastest. It scored 100/100 on csv-scaffolded with a 25-second wall time — another field record. Then it scored 10/100 on fizzbuzz and instant-failed on json-validator in zero turns. The inconsistency pattern makes sense for a model fine-tuned specifically for tool use and short completions: it handles the tasks that match its training profile well and falls apart outside them. For anyone doing rapid-fire chat or simple completions at scale, hermes3 is the answer. For general agentic coding, the brittleness is a real problem.</p> <p><strong>phi4:14b.</strong> 6/13 on single-shot; 100/100 on fizzbuzz and word-freq. It failed markdown-to-html and json-validator, and both failures have the same signature: 16 to 17% context utilization, then the output starts spiraling. phi4 has a 16K context ceiling, and tasks that grow their working context over multiple iterations hit that wall. The context limit is the only thing preventing phi4 from joining the reliable agentic tier. With 32K context or better, I’d expect it to pass everything it currently fails.</p> <p><strong>codestral:22b.</strong> The markdown-to-html task produced a unicode crash — aider’s display layer choked on an arrow character in a CP1252 terminal. json-validator and word-freq both passed 100/100. That markdown failure is an environment bug, not a model failure. I’m counting it in the pass rate because I can’t retroactively change the environment it ran in, but anyone testing codestral in a UTF-8 terminal should expect a different result.</p> <h2 id="the-actual-picks">The Actual Picks</h2> <p>For coding work on a 16GB machine, the answer depends on what you’re doing.</p> <p>If you’re working in a new codebase — multi-file, complex scaffolding, scratch-to-working-tests — use devstral:latest. It’s the only model in this pool that reliably handles multi-file C# from scratch. 83% agentic pass rate across six diverse tasks spanning C# and Python. Not the fastest at 3 to 20 seconds per response, but it has the highest ceiling and it doesn’t fall apart on complexity.</p> <p>If you’re working in an existing codebase — the actual everyday case, where you’re editing files that already exist — use qwen3-coder:30b. 100/100 on Python tasks, strong on scaffolded C#, 2-second code generation. The whole edit format is mandatory; diff mode fails silently and produces nothing. Get the format right and this model is very fast for its size.</p> <p>If VRAM is the constraint, use qwen2.5-coder:14b. It runs on about 9GB, which means it fits alongside other processes. It passed every agentic task I ran it on. The 6/13 single-shot score is misleading — ignore it for agentic work.</p> <p>mistral-small3.2:24b is on a watch list. Three tasks run, three passed. That’s not enough data to promote it above devstral for serious work, but it’s enough to keep it in the rotation. If it holds 100% across ten more tasks I’ll move it up.</p> <p>For chat and Q&amp;A, the picks are different. gemma4:latest for quality — 12/13, fast for its size, clean outputs. Don’t use it for anything agentic. For speed, hermes3:latest at 4.7GB and 280ms code generation is the answer, especially if you’re running it alongside something else or doing high-volume completions.</p> <h2 id="what-single-shot-scores-actually-measure">What Single-Shot Scores Actually Measure</h2> <p>This question came up enough during the project that it deserves a direct answer.</p> <p>Single-shot scores measure whether a model understands what it’s being asked, can produce a well-formed response on one shot, and follows tight output constraints. That’s genuinely useful for chatting, summarizing, classifying, and answering questions. The score is predictive for those tasks.</p> <p>What it does not measure: will this model keep working across turns, will it understand its own previous outputs, can it handle a tool returning an unexpected result, will it know when to stop and verify rather than spiraling, can it write files instead of prose. Those are the capabilities that determine agentic performance. They don’t show up in any single-prompt test because by design they can’t — they require multiple turns to observe.</p> <p>The practical implication is that running a 13-point single-shot harness before picking a coding model will tell you roughly nothing about whether the model can actually do the coding work. You have to run the agentic task. There is no shortcut.</p> <h2 id="closing">Closing</h2> <p>Six weeks. 23 models. 630 lines of harness code. 50+ agentic task runs. The answer to “which local model can actually code?” turns out to be a different question depending on what you mean by coding.</p> <p>The model that tops the single-shot leaderboard is the one to use for chat. The model that wins at agentic coding tasks is a different model entirely. I spent a weekend thinking gemma4 was the obvious answer before it timed out on every real task I gave it.</p> <p>The bench application and all results are at <a href="https://github.com/erichexter/ollama-model-bench">github.com/erichexter/ollama-model-bench</a>. The harness accepts any model Ollama can serve — pull it, add an entry to the settings file, run it. The numbers here are reproducible on any machine with 16GB of VRAM. If you find something that beats devstral on multi-file from scratch, I want to know about it.</p> The Config That Changed Everything https://lostechies.com/erichexter/2026/06/03/local-llm-bench-part-4-harness-optimization/ Los Techies urn:uuid:afdc5df2-d6a6-516d-2ba7-97f98a9329d6 Wed, 03 Jun 2026 12:00:00 +0000 Part 4 of 5 in the Local LLM Bench series. <p><em>Part 4 of 5 in the <a href="/erichexter/2026/05/25/local-llm-bench-part-1-which-models-can-chat/">Local LLM Bench series</a>.</em></p> <p>After Part 3’s 1-in-6 pass rate, I had a theory about qwen3-coder. The model scored 0/100 not because it couldn’t write C#, but because aider couldn’t parse what it wrote. If the failure was format mismatch, then fixing the format should fix the score.</p> <p>I was right. One line in a YAML file took qwen3-coder:30b from 0/100 to 100/100. Twenty-six seconds. Same model, same task, same hardware.</p> <p>That result rewrites how I think about local model evaluation.</p> <h2 id="the-edit_format-lever">The edit_format Lever</h2> <p>aider supports two primary edit modes. In <code class="language-plaintext highlighter-rouge">diff</code> mode, the model sends back git-style patches — only the changed lines, with surrounding context. In <code class="language-plaintext highlighter-rouge">whole</code> mode, the model sends back the entire file contents. These are not stylistic preferences. They require completely different output from the model, and models are not equally capable of both.</p> <p>The research I ran before Phase 9 turned up a finding I didn’t take seriously enough at the time: “harness mismatch is bigger than model choice.” One real-world study cited 6x performance variation purely from harness configuration changes, holding the model constant. I read that and thought it was probably overstated. Then I ran the A/B.</p> <p>The <code class="language-plaintext highlighter-rouge">.aider.model.settings.yml</code> file lets you configure per-model settings. The critical field is <code class="language-plaintext highlighter-rouge">edit_format</code>. Here’s what qwen3-coder’s entry looks like after the fix:</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ollama_chat/qwen3-coder:30b</span> <span class="na">edit_format</span><span class="pi">:</span> <span class="s">whole</span> <span class="na">use_repo_map</span><span class="pi">:</span> <span class="no">false</span> <span class="na">extra_params</span><span class="pi">:</span> <span class="na">num_ctx</span><span class="pi">:</span> <span class="m">65536</span> </code></pre></div></div> <p>Before this change: <code class="language-plaintext highlighter-rouge">edit_format</code> was unset, defaulting to <code class="language-plaintext highlighter-rouge">diff</code>. After: <code class="language-plaintext highlighter-rouge">whole</code>. The model behavior changes completely.</p> <h2 id="the-ab-results">The A/B Results</h2> <p>I ran six models against both formats on the fizzbuzz-plus sweet-spot task:</p> <table> <thead> <tr> <th>Model</th> <th>whole</th> <th>diff</th> </tr> </thead> <tbody> <tr> <td>qwen3-coder:30b</td> <td>100/100 (26s)</td> <td>0/100 FAIL</td> </tr> <tr> <td>devstral:latest</td> <td>100/100 (53s)</td> <td>100/100 (98s)</td> </tr> <tr> <td>qwen2.5-coder:14b</td> <td>100/100 (73s)</td> <td>100/100 (65s)</td> </tr> <tr> <td>gpt-oss:20b</td> <td>20/100 FAIL</td> <td>20/100 FAIL</td> </tr> <tr> <td>qwen3:14b</td> <td>20/100 FAIL</td> <td>20/100 FAIL</td> </tr> <tr> <td>mistral:latest</td> <td>20/100 FAIL</td> <td>20/100 FAIL</td> </tr> </tbody> </table> <p>Three models work. Three models don’t. The format A/B cleanly separates the populations. gpt-oss, qwen3:14b, and mistral fail in both formats — those are genuine capability problems, not configuration problems. qwen3-coder was a false negative: the code was right, the format was wrong, the score said zero.</p> <p>devstral and qwen2.5-coder work in both formats, which tells you something about their training. They’ve been explicitly tuned to produce structured edit blocks. qwen3-coder has not — or at least not in the diff format aider expects. Switching to whole file output removes the constraint entirely: just dump the file, let aider handle the diff computation. qwen3-coder is very good at writing complete, correct files.</p> <h2 id="the-thinking-mode-problem">The Thinking-Mode Problem</h2> <p>Three models that looked promising on paper — gpt-oss:20b, deepseek-r1:14b, and qwen3.5:27b — share a different failure mode. They all run in “thinking mode”: before producing any code output, they generate thousands of internal reasoning tokens. On single-shot tasks this is invisible; the <code class="language-plaintext highlighter-rouge">&lt;think&gt;</code> block appears in a separate field and the user only sees the final answer. On an agentic task with a 300-second timeout, the thinking block alone can exhaust the budget.</p> <p>gpt-oss, deepseek-r1, and qwen3.5 all timeout at zero turns — the model thought itself to death before writing a single line of code.</p> <p>The fix for qwen3 models (not qwen3.5, which has different training) is a <code class="language-plaintext highlighter-rouge">/no_think</code> prefix in the aider system prompt:</p> <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ollama_chat/qwen3:14b</span> <span class="na">edit_format</span><span class="pi">:</span> <span class="s">whole</span> <span class="na">system_prompt_prefix</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/no_think"</span> <span class="na">use_temperature</span><span class="pi">:</span> <span class="m">0.7</span> <span class="na">extra_params</span><span class="pi">:</span> <span class="na">num_ctx</span><span class="pi">:</span> <span class="m">32768</span> <span class="na">top_p</span><span class="pi">:</span> <span class="m">0.8</span> <span class="na">top_k</span><span class="pi">:</span> <span class="m">20</span> </code></pre></div></div> <p>This worked for qwen3:14b and qwen3:30b. It does nothing for qwen3.5 — different model family, different training, the prefix is ignored. qwen3.5:27b is a 17GB model on 16GB VRAM, so it’s partially spilling to RAM anyway. At mixed CPU/GPU generation speed with a thinking block running first, it cannot produce useful output inside 300 seconds. The hardware ceiling and the thinking penalty compound each other. Model eliminated.</p> <h2 id="the-num_ctx-revelation">The num_ctx Revelation</h2> <p>Ollama’s default context window is 2048 tokens. That’s not 2048 for the task — that’s 2048 for the entire conversation, including the system prompt, the file content, the task description, and every prior exchange. For an agentic coding session where aider is sending file contents back and forth, 2048 fills in two or three turns. After that, the model is working with a truncated view of its own conversation. It starts looping, contradicting itself, or deleting code it just wrote.</p> <p>Ollama doesn’t warn you when it truncates. It silently discards the oldest tokens and keeps going. The model’s outputs start looking confused on turn three and you assume it’s a capability problem. It isn’t.</p> <p>Setting <code class="language-plaintext highlighter-rouge">num_ctx: 32768</code> (or 65536 for the larger models) unlocks stable multi-turn behavior. Several failures that looked like model confusion were actually context truncation. The fix is one line per model in the YAML.</p> <h2 id="the-architect-mode-dead-end">The Architect Mode Dead End</h2> <p>I wanted to test whether combining two models — one to plan, one to implement — could improve results on stretch-tier tasks. aider calls this “architect mode.” In principle: the architect model breaks the task into pieces, the editor model writes the code, and the combination should outperform either alone. It’s a reasonable theory. The machine had other plans.</p> <p>Loading two 14-17GB models on 16GB VRAM means constant unloading and reloading. Every time control switches from architect to editor, Ollama has to evict one model and load the other. That swap is not fast. I ran devstral + qwen3-coder and devstral + qwen2.5-coder. Both pairs hit the five-minute timeout at zero turns. The entire budget went to model swap overhead before a single tool call completed.</p> <p>Architect mode requires both models to be co-resident in VRAM. On 16GB, that means two models totaling at most 16GB, which limits you to two 7B models — too small to be useful on complex tasks. The minimum viable VRAM for architect mode with 14B+ models is 32GB. Below that, single-model runs strictly better.</p> <h2 id="the-scaffolding-experiment">The Scaffolding Experiment</h2> <p>After the format A/B produced clear winners, I wanted to understand what was really limiting the failing models on the csv-parser task. The task asked models to build a C# console app and test project from scratch — which means creating <code class="language-plaintext highlighter-rouge">.csproj</code> files, a solution file, adding project references, restoring NuGet packages, and then writing correct C#. That’s two separate problems: .NET project plumbing and C# logic.</p> <p>I split them apart. The scaffolded version of the task pre-creates everything: both <code class="language-plaintext highlighter-rouge">.csproj</code> files with correct <code class="language-plaintext highlighter-rouge">net10.0</code> targets, a <code class="language-plaintext highlighter-rouge">Program.cs</code> entry point the model doesn’t touch, a stub <code class="language-plaintext highlighter-rouge">CsvProcessor.cs</code> with a TODO comment, a test project with a NuGet reference already wired, and stub test method shells. <code class="language-plaintext highlighter-rouge">dotnet restore</code> runs before the model starts. The model’s job is to implement one static method and fill in five test bodies.</p> <table> <thead> <tr> <th>Model</th> <th>From-scratch</th> <th>Scaffolded</th> <th>Change</th> </tr> </thead> <tbody> <tr> <td>devstral:latest</td> <td>70/100</td> <td>90/100</td> <td>+20</td> </tr> <tr> <td>qwen3-coder:30b</td> <td>0/100</td> <td>90/100</td> <td>+90</td> </tr> <tr> <td>cogito:14b</td> <td>0/100</td> <td>10/100</td> <td>+10</td> </tr> <tr> <td>granite4:32b-a9b-h</td> <td>0/100</td> <td>10/100</td> <td>+10</td> </tr> </tbody> </table> <p>qwen3-coder was never broken. Its 0/100 on the from-scratch task was entirely a scaffolding failure. It doesn’t know how to create a .NET solution structure from the command line — that’s a DevOps problem, not a C# problem. Given the structure, it writes correct C# and correct tests in one shot, in 56 seconds. That’s four times faster than devstral on the same task.</p> <p>cogito:14b and granite4:32b-a9b-h still fail on the scaffolded version. Their problem is C# reasoning, not project structure. The scaffolding experiment drew a clean line between the two failure modes.</p> <p>The practical implication: if you’re deploying these models on an existing codebase — the actual real-world use case — the scaffolding problem doesn’t exist. The codebase is already there. qwen3-coder becomes a genuine competitor to devstral for existing-codebase work.</p> <h2 id="where-the-leaderboard-stands">Where the Leaderboard Stands</h2> <p>After format configuration, context window fixes, and scaffolding experiments, the picture looks like this:</p> <p>For sweet-spot tasks (one or two files, existing codebase, 80-120 lines of code): qwen3-coder:30b at 26 seconds, cogito:14b at 11 seconds on both formats, devstral at 53 seconds, mistral-small3.2:24b at 44 seconds, and qwen2.5-coder:14b at 73 seconds. Five models that work reliably.</p> <p>For multi-file from scratch: devstral:latest, confirmed against eight challengers. No other local model in this weight class completes the csv-parser task reliably regardless of configuration.</p> <p>Eliminated regardless of configuration: gemma4 (all variants), glm-4.7-flash, qwen2.5:14b, qwen3:14b, qwen3.5:27b, deepseek-r1, gpt-oss, magistral — all timeout or fail in both formats. These aren’t configuration problems. They’re either the wrong model type (thinking models on a 16GB budget), capability gaps, or both.</p> <p>The 6x performance variation claim from the research turned out to be conservative in at least one case. qwen3-coder went from zero to perfect. You can’t express that as a multiplier.</p> <hr /> <p><em>Next up: <a href="/erichexter/2026/06/06/local-llm-bench-part-5-final-picks/">Part 5</a> — expanding the model pool, three surprise entries that research told me to skip, and the final leaderboard after 23 models across six weeks of testing.</em></p> Single-Shot Lies https://lostechies.com/erichexter/2026/05/31/local-llm-bench-part-3-single-shot-lies/ Los Techies urn:uuid:a9a8a411-09a5-c5e4-97df-4efa81b9ec57 Sun, 31 May 2026 12:00:00 +0000 Part 3 of 5 in the Local LLM Bench series. <p><em>Part 3 of 5 in the <a href="/erichexter/2026/05/25/local-llm-bench-part-1-which-models-can-chat/">Local LLM Bench series</a>.</em></p> <p>gemma4:latest scored 10/10 on every test I built. Perfect chat response. Perfect code generation. Perfect tool call. Perfect instruction following. I ran it twice to be sure. Same result. So naturally, when it came time to run the first real agentic coding task, that was the model I reached for.</p> <p>It produced zero lines of useful code in ten minutes.</p> <p>That’s the story of Phase 8, and it changed everything about how I think about model evaluation.</p> <h2 id="the-task">The Task</h2> <p>The agentic benchmark I built is a CSV parser in C#. A console app that reads a file with Name and Score columns, prints the top 3 scores descending, ties broken alphabetically. Verify with <code class="language-plaintext highlighter-rouge">dotnet test</code>. The task is sized to what I’d call “stretch tier” — two projects, roughly 150 lines of code, multi-file, requires the model to scaffold a .NET solution from scratch and then implement correct logic. A competent human developer does this in about ten minutes.</p> <p>The harness is aider 0.86.2 installed via <code class="language-plaintext highlighter-rouge">uv tools</code>, running headless with <code class="language-plaintext highlighter-rouge">--yes-always --exit --message-file</code>. Scoring: 60 points if the verify command passes, 20 if the model finishes in two iterations or fewer, 10 for no compile errors, 10 for clean edit format. 100 points maximum.</p> <p>I ran six models: the top performers from Phase 4’s single-shot benchmark plus two new additions.</p> <h2 id="the-results">The Results</h2> <table> <thead> <tr> <th>Model</th> <th>Score</th> <th>Notes</th> </tr> </thead> <tbody> <tr> <td>devstral:latest</td> <td>70/100</td> <td>5 iterations, 147 seconds</td> </tr> <tr> <td>gemma4:latest</td> <td>20/100</td> <td>600 second timeout, 0 turns</td> </tr> <tr> <td>gemma4:26b</td> <td>20/100</td> <td>600 second timeout</td> </tr> <tr> <td>glm-4.7-flash</td> <td>20/100</td> <td>600 second timeout</td> </tr> <tr> <td>qwen2.5:14b</td> <td>10/100</td> <td>91 seconds, never recovered</td> </tr> <tr> <td>qwen3-coder:30b</td> <td>0/100</td> <td>77 seconds, garbled output</td> </tr> </tbody> </table> <p>One passes. Five fail. The model that aced every single-shot test I designed hits its ten-minute wall and produces nothing. The model that topped the leaderboard with a perfect score is the first casualty.</p> <p>devstral is, notably, marketed specifically for agentic coding loops. That framing turned out to matter.</p> <h2 id="what-went-wrong-with-gemma4">What Went Wrong With gemma4</h2> <p>gemma4:latest doesn’t fail because it can’t write C#. It fails because it doesn’t understand that it’s supposed to be writing files. When aider sends it a task, it responds with a description of what the code should look like, or it writes a fenced code block in prose, or it explains the approach in detail without producing any actual edits. I watched this happen in real time and it took longer than I’d like to admit before I understood what I was seeing. These responses look helpful if you’re reading them as a chat assistant. aider can’t do anything with them — it’s waiting for structured edit blocks that follow its protocol, not a tutorial.</p> <p>The single-shot benchmark rewarded exactly the behavior that makes gemma4 useless in an agentic loop. “Write a Python function that checks if a number is prime” — gemma4 produces clean, correct Python instantly. But that task has one shot, one context, one output. There’s no concept of a multi-step session, no expectation that the model needs to write files into a directory, no loop where the model gets feedback and tries again.</p> <p>Ask gemma4 to run a ten-minute coding session and it has no mental model for what “running a coding session” means. It’s a very good chat assistant. That’s not the same thing.</p> <h2 id="what-went-wrong-with-qwen3-coder">What Went Wrong With qwen3-coder</h2> <p>qwen3-coder:30b scores 0/100, which looks worse than the timeout failures. It’s actually more interesting. The model ran for 77 seconds before aider gave up, which means it produced output — just output that aider silently rejected as malformed edits. The code was probably fine. The format wasn’t.</p> <p>This is a harness compatibility problem, not a capability problem. aider expects edit blocks in specific formats — either a <code class="language-plaintext highlighter-rouge">diff</code>-style patch or a <code class="language-plaintext highlighter-rouge">whole</code>-file replacement. qwen3-coder was emitting something that resembled neither cleanly enough for aider to parse. aider’s response to a malformed edit is to silently skip it, log nothing useful, and eventually exit. From the score sheet, it looks like the model produced nothing. That’s not what happened.</p> <p>This distinction matters, because it’s a clue. If the failure is format mismatch rather than capability, changing the format instruction should fix it. I filed that away and moved on.</p> <h2 id="what-it-means">What It Means</h2> <p>The research literature on agentic coding benchmarks describes a roughly 17% pass rate for 14-30B parameter models on what they call “stretch tier” tasks: multi-file, 150+ lines of code, multiple tool-call iterations. My six-model run hit 1-in-6. Exactly 17%.</p> <p>That number didn’t come from luck. It came from the same thing the research describes: most models that can answer questions well don’t have a working mental model of “I am operating a computer, I need to write files, I need to keep doing work until a test passes.” Those are different cognitive tasks. Single-shot chat benchmarks don’t distinguish between them.</p> <p>The models that time out aren’t slower or dumber than devstral. They’re not designed for this. gemma4 is optimized to produce a high-quality response to a question. devstral is optimized to take a task and not stop until it’s done. The training objectives are different. The behavior is different. The single-shot score captures none of that.</p> <h2 id="where-this-leaves-us">Where This Leaves Us</h2> <p>devstral finished the task with 70/100. It needed five iterations instead of two (losing 20 points on the efficiency score), but it shipped working code. None of the other five models produced a single passing test.</p> <p>The 70/100 score isn’t a ceiling — it’s a baseline. devstral used the default aider configuration with no tuning. It worked anyway. The question is whether anything else can be made to work, or whether devstral is the only local model that can do this at all.</p> <p>qwen3-coder’s format failure points toward an answer. If the problem is configuration, not capability, then changing the configuration should change the result. That’s the experiment Part 4 runs.</p> <hr /> <p><em>Next up: <a href="/erichexter/2026/06/03/local-llm-bench-part-4-harness-optimization/">Part 4</a> — one config change takes a model from 0/100 to 100/100, and the harness turns out to matter more than the model.</em></p> Building a .NET 10 Benchmark Harness https://lostechies.com/erichexter/2026/05/28/local-llm-bench-part-2-building-the-harness/ Los Techies urn:uuid:e0f1e69c-464b-bd56-919f-62b42609c4b5 Thu, 28 May 2026 12:00:00 +0000 Part 2 of 5 in the Local LLM Bench series. <p><em>Part 2 of 5 in the <a href="/erichexter/2026/05/25/local-llm-bench-part-1-which-models-can-chat/">Local LLM Bench series</a>.</em></p> <p>The PowerShell script from part one did its job. It surfaced the think-mode problem, sorted out which models could call tools, and gave me rough latency numbers. But it could not tell me whether the code models wrote was actually correct — I was reading output and deciding it looked fine, which is not the same thing as running it.</p> <p>What I needed was a harness that ran models against defined tasks, verified the outputs mechanically, and produced a repeatable score. I’m a C# developer. .NET 10 was already on the machine. The choice was not a choice.</p> <h2 id="architecture">Architecture</h2> <p>The project is a .NET 10 console application. The core pieces are:</p> <p><strong>OllamaRunner</strong> is a thin HTTP wrapper around Ollama’s <code class="language-plaintext highlighter-rouge">/api/generate</code> and <code class="language-plaintext highlighter-rouge">/api/chat</code> endpoints. Every request goes out with <code class="language-plaintext highlighter-rouge">temperature=0</code>, <code class="language-plaintext highlighter-rouge">seed=42</code>, and <code class="language-plaintext highlighter-rouge">think=false</code>. Temperature zero makes results deterministic enough to compare across runs. The seed locks that in further. The <code class="language-plaintext highlighter-rouge">think</code> flag is false by default — models that need it explicitly will be detected and handled.</p> <p><strong>RoslynEvaluator</strong> handles the <code class="language-plaintext highlighter-rouge">SumEvens</code> code test in-process. It takes whatever the model returns, strips any markdown fences, wraps the bare method in a class, and hands it to the Roslyn CSharp scripting API to compile and execute. If it compiles and <code class="language-plaintext highlighter-rouge">SumEvens(new[] {1,2,3,4,5})</code> returns 6, the model passes. This runs entirely in memory with no disk I/O and no subprocess.</p> <p><strong>TempProjectRunner</strong> is where it gets more serious. This component scaffolds actual temporary <code class="language-plaintext highlighter-rouge">dotnet</code> projects, writes model-generated code into them, builds them with <code class="language-plaintext highlighter-rouge">dotnet build</code>, and runs them with <code class="language-plaintext highlighter-rouge">dotnet run</code>. It checks stdout for the expected output. For the test suite portion, it scaffolds a second project alongside the first, adds a project reference, drops in model-generated xUnit test code, and runs <code class="language-plaintext highlighter-rouge">dotnet test</code>. Every project is cleaned up from the temp directory when the run completes.</p> <p><strong>Scorer</strong> orchestrates the sequence — chat test, code test, tool test, instruction test, reasoning test, JSON output test, sequence test, Hello World test — and assembles the results into a <code class="language-plaintext highlighter-rouge">ModelResult</code> record.</p> <p><strong>ModelResult</strong> is a straightforward C# record type. Every boolean metric is a property; <code class="language-plaintext highlighter-rouge">TotalScore</code> is a computed getter that sums them. The record also carries timing in milliseconds for each test category and a <code class="language-plaintext highlighter-rouge">ThinkRequired</code> flag that is informational only and does not affect the score.</p> <p><strong>ConsoleReporter</strong> prints the final table to the terminal with ANSI color coding. <strong>ResultStore</strong> writes the raw results to <code class="language-plaintext highlighter-rouge">results/model-results.json</code> and a human-readable markdown ledger to <code class="language-plaintext highlighter-rouge">results/RESULTS.md</code> after each run.</p> <h2 id="the-code-tests">The Code Tests</h2> <p>The first code test is <code class="language-plaintext highlighter-rouge">SumEvens</code>: write a C# method that takes <code class="language-plaintext highlighter-rouge">IEnumerable&lt;int&gt;</code> and returns the sum of even numbers. Return only the method, no class, no namespace, no explanation. This is deliberately narrow. The narrow scope is the point — it is testing whether a model can follow output constraints and write code that compiles and produces correct results, not whether it can write impressive prose around the code.</p> <p>RoslynEvaluator wraps the method in a class, invokes it with <code class="language-plaintext highlighter-rouge">{1, 2, 3, 4, 5}</code>, and checks that the result is 6. Compile error means the model scores zero on both compile and correct. Compiles but returns the wrong number means compile point awarded, correct point denied. Compiles and returns 6 means full credit.</p> <h2 id="hello-world-the-real-test">Hello World: The Real Test</h2> <p>The Hello World test is where I learned something useful. The prompt asks the model to write a complete C# console application: a <code class="language-plaintext highlighter-rouge">Greeter</code> class with a public static <code class="language-plaintext highlighter-rouge">GetGreeting()</code> method that returns <code class="language-plaintext highlighter-rouge">"Hello, World!"</code>, plus a Main method or top-level statements that calls it and prints the result. Separately, it asks the model to write xUnit tests for that <code class="language-plaintext highlighter-rouge">Greeter</code> class.</p> <p>TempProjectRunner scaffolds a <code class="language-plaintext highlighter-rouge">dotnet new console</code> project, replaces <code class="language-plaintext highlighter-rouge">Program.cs</code> with whatever the model generated, runs <code class="language-plaintext highlighter-rouge">dotnet build</code>, then <code class="language-plaintext highlighter-rouge">dotnet run</code>, and checks stdout for <code class="language-plaintext highlighter-rouge">"Hello, World!"</code>. For the test portion, it scaffolds a <code class="language-plaintext highlighter-rouge">dotnet new xunit</code> project in the same temp directory, adds a project reference to the app, drops in the model’s test code as <code class="language-plaintext highlighter-rouge">GreeterTests.cs</code>, runs <code class="language-plaintext highlighter-rouge">dotnet build</code>, and then <code class="language-plaintext highlighter-rouge">dotnet test</code>.</p> <p>This turns out to be an excellent proxy for whether a model understands C# project structure. Writing a method is straightforward. Writing a complete application that builds from scratch against a specific framework target, with a class in a form that a separately compiled test project can reference — that is a different problem. Models that understand C# project conventions get it right on the first try. Models that pattern-match on superficial features tend to include the wrong using statements, declare the class in a namespace that the test code does not account for, or produce an entry point that conflicts with the <code class="language-plaintext highlighter-rouge">Greeter</code> class definition.</p> <p>Each step is gated: if the app does not compile, neither the output check nor the test run happens. If the tests do not compile, the pass/fail result is not recorded. Partial credit is possible — a model can build the app but write tests that compile and then fail at runtime, earning two of the four Hello World points.</p> <h2 id="scoring">Scoring</h2> <p>The 10-point scoring breakdown for the initial complete run:</p> <table> <thead> <tr> <th>Category</th> <th>Points</th> </tr> </thead> <tbody> <tr> <td>Chat response (non-empty, sensible)</td> <td>1</td> </tr> <tr> <td>SumEvens compiles</td> <td>1</td> </tr> <tr> <td>SumEvens correct</td> <td>1</td> </tr> <tr> <td>Tool call supported (not HTTP 400)</td> <td>1</td> </tr> <tr> <td>Tool call valid (structured, correct function)</td> <td>1</td> </tr> <tr> <td>Instruction followed (exactly three words)</td> <td>1</td> </tr> <tr> <td>Hello World app compiles</td> <td>1</td> </tr> <tr> <td>Hello World app correct output</td> <td>1</td> </tr> <tr> <td>Hello World tests compile</td> <td>1</td> </tr> <tr> <td>Hello World tests pass</td> <td>1</td> </tr> </tbody> </table> <p>After the initial runs I extended the suite with three more tests, bringing the maximum to 13: a reasoning test (a word problem with an exact numeric answer — $4.50, no other text), a JSON output test (produce a valid JSON array of at least three programming language names), and a sequence test (output the numbers 1 through 5, one per line, nothing else). All three are binary pass/fail with no partial credit. The reasoning and sequence tests catch models that ignore output constraints even when the constraint is explicit. Several did.</p> <h2 id="unit-tests">Unit Tests</h2> <p>The test project covers 13 cases across five test classes. <code class="language-plaintext highlighter-rouge">ModelResultTests</code> verifies that the scoring logic is correct — all true returns the expected sum, all false returns zero, <code class="language-plaintext highlighter-rouge">ThinkRequired</code> does not affect the score. <code class="language-plaintext highlighter-rouge">RoslynEvaluatorTests</code> covers the markdown fence stripping and three evaluation cases: correct implementation, wrong result, and garbage input. <code class="language-plaintext highlighter-rouge">ScorerTests</code> uses a <code class="language-plaintext highlighter-rouge">MockRunner</code> that replays canned responses and verifies that the Scorer assembles the <code class="language-plaintext highlighter-rouge">ModelResult</code> correctly for the pass case, the tool-rejected case, and the instruction-failure case. <code class="language-plaintext highlighter-rouge">ConsoleReporterTests</code> confirms that <code class="language-plaintext highlighter-rouge">PrintTable</code> does not throw with null prior results or when a model has regressed since the previous run.</p> <p>None of these tests require a running Ollama instance. The mock runner pattern makes the Scorer fully testable without any external dependencies.</p> <h2 id="first-complete-run">First Complete Run</h2> <p>Thirteen models, ten metrics each. This is what came back:</p> <table> <thead> <tr> <th>Model</th> <th>Score</th> <th>Notes</th> </tr> </thead> <tbody> <tr> <td>gemma4:latest</td> <td>10/10</td> <td>Clean sweep</td> </tr> <tr> <td>glm-4.7-flash</td> <td>9/10</td> <td> </td> </tr> <tr> <td>gemma4:26b</td> <td>8/10</td> <td> </td> </tr> <tr> <td>qwen2.5:14b</td> <td>8/10</td> <td> </td> </tr> <tr> <td>devstral:latest</td> <td>7/10</td> <td> </td> </tr> <tr> <td>qwen3-coder:30b</td> <td>7/10</td> <td> </td> </tr> <tr> <td>qwen3:14b</td> <td>7/10</td> <td> </td> </tr> <tr> <td>mistral:latest</td> <td>6/10</td> <td> </td> </tr> <tr> <td>gpt-oss:20b</td> <td>5/10</td> <td>think_required detected</td> </tr> <tr> <td>phi4:14b</td> <td>5/10</td> <td> </td> </tr> <tr> <td>llava:7b</td> <td>5/10</td> <td> </td> </tr> <tr> <td>qwen2.5-coder:14b</td> <td>4/10</td> <td> </td> </tr> <tr> <td>qwen3:30b</td> <td>3/10</td> <td> </td> </tr> </tbody> </table> <p>gemma4:latest — a ~12B parameter model — scores 10 out of 10. It answers the chat question, writes <code class="language-plaintext highlighter-rouge">SumEvens</code> correctly, emits a proper tool call, follows the three-word instruction, builds the Hello World app, writes tests that compile and pass, gets the math problem right, produces valid JSON, and outputs the sequence with no extra text. On every metric the harness defines, it is the best model in the pool by a clean margin over everything larger than it.</p> <p>The result is worth sitting with. A model less than half the size of qwen3:30b outscores it by seven points. glm-4.7-flash is a 30B MoE and comes in second at 9/10. The coding-focused variants — qwen2.5-coder and qwen3-coder — score lower than their general-purpose counterparts at similar sizes.</p> <p>The obvious interpretation is that gemma4:latest is simply the best model here. The problem is that the harness measures what I built the harness to measure. Before drawing that conclusion, I need to know whether these metrics are the right metrics.</p> <hr /> <p>The full source is at <a href="https://github.com/erichexter/ollama-model-bench">github.com/erichexter/ollama-model-bench</a>.</p> <hr /> <p><em>Next up: <a href="/erichexter/2026/05/31/local-llm-bench-part-3-single-shot-lies/">Part 3</a> digs into what the scores actually mean — and why gemma4:latest’s clean sweep turned out to be almost entirely beside the point.</em></p> Search — The Evolution of the Karpathy LLM Wiki https://lostechies.com/erichexter/2026/05/26/search-evolution-of-the-karpathy-llm-wiki/ Los Techies urn:uuid:f45508c1-db67-a6d3-2b7e-6800ad986068 Tue, 26 May 2026 12:00:00 +0000 My LLM notes wiki outgrew file reads. Agents were pulling entire files to find a single relevant section — burning tokens on context that didn’t matter, missing things that were buried three pages deep. The corpus had just grown past the point where IO-based access was practical. <p>My LLM notes wiki outgrew file reads. Agents were pulling entire files to find a single relevant section — burning tokens on context that didn’t matter, missing things that were buried three pages deep. The corpus had just grown past the point where IO-based access was practical.</p> <p>The fix was search. And since agents need tools, the obvious move was to build it as an MCP server. But if you’re building search anyway, plain keyword matching felt like leaving half the value on the table — too easy to miss conceptual matches that don’t share exact terms. So: something old and something new. SQLite already has FTS5. sqlite-vec adds HNSW vector search as a loadable extension. Ollama runs the embedding model locally. Put them together and you get hybrid RAG on hardware you already own, exposed as an MCP tool any agent in the fleet can call.</p> <p>This post covers how it’s built — starting from what the agent sees and working inward to the SQL and vector embeddings.</p> <hr /> <h2 id="what-the-agent-sees">What the Agent Sees</h2> <p>From the agent’s perspective, this is just an MCP server with a set of tools. Point an <code class="language-plaintext highlighter-rouge">.mcp.json</code> at the host and the tools are available. No setup, no SDK, no awareness of what’s running underneath.</p> <p>The primary tool is <code class="language-plaintext highlighter-rouge">search_knowledge</code>:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tools/call"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"search_knowledge"</span><span class="p">,</span><span class="w"> </span><span class="nl">"arguments"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"attention mechanism scaled dot product"</span><span class="p">,</span><span class="w"> </span><span class="nl">"top_k"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w"> </span><span class="nl">"hybrid_alpha"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.6</span><span class="p">,</span><span class="w"> </span><span class="nl">"sources"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"karpathy-wiki"</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>The response comes back as ranked chunks with source context:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text"</span><span class="p">,</span><span class="w"> </span><span class="nl">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[ { </span><span class="se">\"</span><span class="s2">text</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">Scaled dot-product attention divides the dot products by √d_k to prevent vanishing gradients in high dimensions...</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">source</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">karpathy-wiki</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">relPath</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="s2">transformers/attention.md</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">score</span><span class="se">\"</span><span class="s2">: 0.91, </span><span class="se">\"</span><span class="s2">frontmatter</span><span class="se">\"</span><span class="s2">: { </span><span class="se">\"</span><span class="s2">tags</span><span class="se">\"</span><span class="s2">: [</span><span class="se">\"</span><span class="s2">attention</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">transformers</span><span class="se">\"</span><span class="s2">] } }, ... ]"</span><span class="w"> </span><span class="p">}]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>The agent gets ranked text chunks, source file paths, and scores. It doesn’t need to know whether the result came from a vector search or keyword search — that’s the server’s problem.</p> <h3 id="the-full-tool-set">The Full Tool Set</h3> <p>Seven tools in total. <code class="language-plaintext highlighter-rouge">search_knowledge</code> covers 95% of use.</p> <table> <thead> <tr> <th>Tool</th> <th>Purpose</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">search_knowledge</code></td> <td>Hybrid vec+FTS search across one or more sources.</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">get_page</code></td> <td>Retrieve a full page by source + relative path. Use when search returns a partial chunk and you want the full document.</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">list_sources</code></td> <td>Lists indexed sources with page/chunk counts and last-indexed timestamps.</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">get_stats</code></td> <td>Query counts and latencies over 1h / 24h / 7d / 30d windows.</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">get_query_log</code></td> <td>Recent query history. Useful for understanding what agents are actually asking.</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">refresh_ingest</code></td> <td>Trigger immediate re-indexing for a source after a write.</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">ping</code></td> <td>Returns current UTC. Health check.</td> </tr> </tbody> </table> <p><code class="language-plaintext highlighter-rouge">list_sources</code> is underrated as a diagnostic. A 200 response from the API tells you nothing about whether the index is populated. If results are poor, check <code class="language-plaintext highlighter-rouge">pageCount &gt; 0</code> and that <code class="language-plaintext highlighter-rouge">lastIndexed</code> is recent before assuming the search logic is wrong.</p> <h3 id="the-hybrid_alpha-parameter">The <code class="language-plaintext highlighter-rouge">hybrid_alpha</code> Parameter</h3> <p>This is the control knob for the blend between vector search and full-text search.</p> <ul> <li><code class="language-plaintext highlighter-rouge">0.0</code> — pure FTS (BM25 keyword ranking)</li> <li><code class="language-plaintext highlighter-rouge">1.0</code> — pure vector (semantic similarity)</li> <li><code class="language-plaintext highlighter-rouge">0.5</code> — equal blend (default)</li> </ul> <p>In practice, <code class="language-plaintext highlighter-rouge">0.6</code>–<code class="language-plaintext highlighter-rouge">0.7</code> (vector-weighted) works better for conceptual queries: “how does attention scale with sequence length.” Drop toward <code class="language-plaintext highlighter-rouge">0.3</code> when you need an exact term match that the embedding model might paraphrase: specific function names, error codes, version numbers.</p> <hr /> <h2 id="how-the-search-works">How the Search Works</h2> <p>When <code class="language-plaintext highlighter-rouge">search_knowledge</code> is called, the server runs two queries in parallel and merges the results.</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">vectorTask</span> <span class="p">=</span> <span class="nf">SearchByVector</span><span class="p">(</span><span class="n">embeddingVector</span><span class="p">,</span> <span class="n">topK</span> <span class="p">*</span> <span class="m">2</span><span class="p">,</span> <span class="n">sources</span><span class="p">);</span> <span class="kt">var</span> <span class="n">ftsTask</span> <span class="p">=</span> <span class="nf">SearchByFts</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">topK</span> <span class="p">*</span> <span class="m">2</span><span class="p">,</span> <span class="n">sources</span><span class="p">);</span> <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">WhenAll</span><span class="p">(</span><span class="n">vectorTask</span><span class="p">,</span> <span class="n">ftsTask</span><span class="p">);</span> <span class="kt">var</span> <span class="n">merged</span> <span class="p">=</span> <span class="nf">Merge</span><span class="p">(</span><span class="n">vectorTask</span><span class="p">.</span><span class="n">Result</span><span class="p">,</span> <span class="n">ftsTask</span><span class="p">.</span><span class="n">Result</span><span class="p">,</span> <span class="n">hybridAlpha</span><span class="p">,</span> <span class="n">topK</span><span class="p">);</span> </code></pre></div></div> <p>The merge step normalizes each result list’s scores to <code class="language-plaintext highlighter-rouge">[0, 1]</code>, applies the alpha weight, sums scores per chunk (a chunk can appear in both lists), and returns the top K. Normalization matters — BM25 and HNSW distance are on completely different scales. Skip it and one path dominates every query regardless of alpha.</p> <p>Before either query runs, the search query itself gets embedded:</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST http://&lt;ollama-host&gt;:11434/api/embeddings Content-Type: application/json { "model": "nomic-embed-text:latest", "prompt": "attention mechanism scaled dot product" } </span></code></pre></div></div> <p>That gives back a 768-dimensional float vector — what the vector search runs against.</p> <h3 id="the-vector-query">The Vector Query</h3> <p>sqlite-vec exposes vector search through a virtual table with a <code class="language-plaintext highlighter-rouge">MATCH</code> clause. Under the hood it’s doing an approximate nearest-neighbor scan via HNSW:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">body</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="k">source</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">rel_path</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">frontmatter</span><span class="p">,</span> <span class="n">cv</span><span class="p">.</span><span class="n">distance</span> <span class="k">FROM</span> <span class="n">chunk_vecs</span> <span class="n">cv</span> <span class="k">JOIN</span> <span class="n">chunks</span> <span class="k">c</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">cv</span><span class="p">.</span><span class="n">chunk_id</span> <span class="k">WHERE</span> <span class="n">cv</span><span class="p">.</span><span class="n">embedding</span> <span class="k">MATCH</span> <span class="p">:</span><span class="n">embedding</span> <span class="k">AND</span> <span class="n">cv</span><span class="p">.</span><span class="n">k</span> <span class="o">=</span> <span class="p">:</span><span class="n">k</span> <span class="k">AND</span> <span class="p">(:</span><span class="n">sources</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="k">c</span><span class="p">.</span><span class="k">source</span> <span class="k">IN</span> <span class="p">:</span><span class="n">sources</span><span class="p">)</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">cv</span><span class="p">.</span><span class="n">distance</span><span class="p">;</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">distance</code> here is L2 distance — lower is closer. sqlite-vec handles all the index internals; from the query side it looks like a regular SQL query.</p> <h3 id="the-fts-query">The FTS Query</h3> <p>Standard SQLite FTS5 with BM25 ranking:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">body</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="k">source</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">rel_path</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">frontmatter</span><span class="p">,</span> <span class="n">bm25</span><span class="p">(</span><span class="n">chunk_fts</span><span class="p">)</span> <span class="k">AS</span> <span class="n">fts_score</span> <span class="k">FROM</span> <span class="n">chunk_fts</span> <span class="k">JOIN</span> <span class="n">chunks</span> <span class="k">c</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">chunk_fts</span><span class="p">.</span><span class="n">rowid</span> <span class="k">WHERE</span> <span class="n">chunk_fts</span> <span class="k">MATCH</span> <span class="p">:</span><span class="n">query</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">bm25</span><span class="p">(</span><span class="n">chunk_fts</span><span class="p">)</span> <span class="k">LIMIT</span> <span class="p">:</span><span class="n">k</span><span class="p">;</span> </code></pre></div></div> <p>FTS5’s <code class="language-plaintext highlighter-rouge">MATCH</code> supports phrase queries, prefix matching, and boolean operators. For agent queries coming in as natural language, the server sanitizes the input to a simple term query before passing it to MATCH.</p> <hr /> <h2 id="the-data-model">The Data Model</h2> <p>Three tables carry the retrieval workload:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Chunked text with metadata</span> <span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">chunks</span> <span class="p">(</span> <span class="n">id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">page_id</span> <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">REFERENCES</span> <span class="n">pages</span><span class="p">(</span><span class="n">id</span><span class="p">),</span> <span class="n">chunk_index</span> <span class="nb">INTEGER</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span> <span class="n">body</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span> <span class="n">token_count</span> <span class="nb">INTEGER</span><span class="p">,</span> <span class="k">source</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">rel_path</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">frontmatter</span> <span class="nb">TEXT</span> <span class="p">);</span> <span class="c1">-- Vector index (sqlite-vec extension)</span> <span class="k">CREATE</span> <span class="n">VIRTUAL</span> <span class="k">TABLE</span> <span class="n">chunk_vecs</span> <span class="k">USING</span> <span class="n">vec0</span><span class="p">(</span> <span class="n">chunk_id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">embedding</span> <span class="nb">FLOAT</span><span class="p">[</span><span class="mi">768</span><span class="p">]</span> <span class="p">);</span> <span class="c1">-- Full-text search index (FTS5, built into SQLite)</span> <span class="k">CREATE</span> <span class="n">VIRTUAL</span> <span class="k">TABLE</span> <span class="n">chunk_fts</span> <span class="k">USING</span> <span class="n">fts5</span><span class="p">(</span> <span class="n">body</span><span class="p">,</span> <span class="k">source</span> <span class="n">UNINDEXED</span><span class="p">,</span> <span class="n">rel_path</span> <span class="n">UNINDEXED</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s1">'chunks'</span><span class="p">,</span> <span class="n">content_rowid</span><span class="o">=</span><span class="s1">'id'</span> <span class="p">);</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">chunk_vecs</code> is a <a href="https://github.com/asg017/sqlite-vec">sqlite-vec</a> <code class="language-plaintext highlighter-rouge">vec0</code> virtual table — INSERT a row with the chunk ID and its 768-dim embedding, sqlite-vec maintains the HNSW index internally. <code class="language-plaintext highlighter-rouge">chunk_fts</code> is a content-backed FTS5 table that stays in sync with <code class="language-plaintext highlighter-rouge">chunks</code> via triggers.</p> <p>Supporting tables: <code class="language-plaintext highlighter-rouge">pages</code> (source files with hash-based change detection), <code class="language-plaintext highlighter-rouge">indexer_runs</code> (ingest audit log), <code class="language-plaintext highlighter-rouge">query_log</code> (query history for observability).</p> <p>One SQLite file. No separate processes, no network hops between storage components, no backup complexity.</p> <hr /> <h2 id="the-write-path">The Write Path</h2> <p>When a document is added or updated in the source directory, the indexer picks it up:</p> <ol> <li>SHA-256 hash the file. Compare against <code class="language-plaintext highlighter-rouge">pages.content_hash</code>. Skip if unchanged.</li> <li>Parse YAML frontmatter. Extract the body.</li> <li>Split into chunks — 512-token target, 64-token overlap, break on paragraph boundaries where possible.</li> <li>For each chunk: POST to Ollama <code class="language-plaintext highlighter-rouge">/api/embeddings</code>. Receive a 768-dim float array.</li> <li>INSERT into <code class="language-plaintext highlighter-rouge">chunks</code>. INSERT into <code class="language-plaintext highlighter-rouge">chunk_vecs</code>. FTS5 trigger handles <code class="language-plaintext highlighter-rouge">chunk_fts</code> sync.</li> <li>Update <code class="language-plaintext highlighter-rouge">pages.content_hash</code> and <code class="language-plaintext highlighter-rouge">indexed_at</code>.</li> <li>Write a row to <code class="language-plaintext highlighter-rouge">indexer_runs</code>.</li> </ol> <p><code class="language-plaintext highlighter-rouge">nomic-embed-text</code> is 137M parameters — fast on a GPU host, single-digit milliseconds per chunk. The indexer pipelines requests; Ollama queues them.</p> <hr /> <h2 id="gotchas">Gotchas</h2> <p><strong>The embed model context limit is a silent failure.</strong></p> <p><code class="language-plaintext highlighter-rouge">nomic-embed-text</code> has an 8K token context window. Chunks that exceed it are silently not embedded — present in <code class="language-plaintext highlighter-rouge">chunks</code>, retrievable via <code class="language-plaintext highlighter-rouge">get_page</code>, invisible to vector search. No error from Ollama. Enforce the chunk size limit at ingest time. Symptom check:</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">p</span><span class="p">.</span><span class="n">rel_path</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="k">source</span><span class="p">,</span> <span class="k">LENGTH</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">content</span><span class="p">)</span> <span class="k">A Which Local Models Can Actually Code? https://lostechies.com/erichexter/2026/05/25/local-llm-bench-part-1-which-models-can-chat/ Los Techies urn:uuid:3d5ab136-27b3-4dcd-6cc6-8f24820efc78 Mon, 25 May 2026 12:00:00 +0000 Part 1 of 5 in the Local LLM Bench series. <p><em>Part 1 of 5 in the <a href="/erichexter/2026/05/25/local-llm-bench-part-1-which-models-can-chat/">Local LLM Bench series</a>.</em></p> <p>I had ten local models installed and no good answer to a simple question: which of them could actually do useful work? Chat demos are easy to fake. I wanted to know whether these models could write working code, call tools correctly, and follow instructions without needing hand-holding. The only way to find out was to run them.</p> <h2 id="the-setup">The Setup</h2> <p>Machine is an Alienware Windows 11 box with an RTX 5080 carrying 16GB of VRAM. Ollama is running locally, serving the following ten models:</p> <ul> <li>mistral:latest (7B)</li> <li>llava:7b (7B, vision)</li> <li>gemma4:latest (~12B)</li> <li>gemma4:26b (26B)</li> <li>qwen3:14b (14B)</li> <li>qwen3:30b (30B)</li> <li>phi4:14b (14B)</li> <li>qwen2.5:14b (14B)</li> <li>qwen2.5-coder:14b (14B, coding-focused)</li> <li>glm-4.7-flash (30B MoE)</li> </ul> <p>The size range alone tells you the hardware story. Anything under about 20B fits in VRAM comfortably. The 26B and 30B models spill onto system RAM — which you feel in the latency numbers.</p> <h2 id="first-pass-two-prompts-powershell">First Pass: Two Prompts, PowerShell</h2> <p>The first script was about as minimal as it gets. Two prompts per model: “What is the capital of France?” to confirm the model is responding at all, and “Write an <code class="language-plaintext highlighter-rouge">is_prime()</code> function in Python” as a basic code generation check. No scoring, no verification — just checking that something came back.</p> <p>Most models answered both prompts without incident. Then I hit the bigger ones. gemma4:26b, glm-4.7-flash, and qwen3:30b all returned empty responses. Not errors — the HTTP calls succeeded, Ollama said everything was fine, the responses just contained no text.</p> <p>That took longer than it should have, and the answer was different for each model.</p> <h2 id="the-think-mode-wall">The Think-Mode Wall</h2> <p>qwen3 models support a reasoning mode where the model works through a problem step by step before producing visible output. The reasoning tokens live inside <code class="language-plaintext highlighter-rouge">&lt;think&gt;...&lt;/think&gt;</code> blocks and don’t count against the response. What does count against the response is the token budget, and when I was requesting with a tight <code class="language-plaintext highlighter-rouge">num_predict</code> limit, the model was spending the entire budget on internal reasoning and returning nothing to the caller. glm-4.7-flash has its own variant of the same mode — different model family, same symptom.</p> <p>The fix for both: add <code class="language-plaintext highlighter-rouge">"think": false</code> to the request body. With that flag set, qwen3:14b went from returning a blank response to producing clean, working code in about 2 seconds. The qwen3 and glm models followed.</p> <p>gemma4:26b’s blank responses were a separate problem entirely. At 26B it spills to RAM, and with a tight <code class="language-plaintext highlighter-rouge">num_predict</code> budget and slow generation speed, the script’s read timeout was firing before any tokens arrived. More headroom fixed it.</p> <p>The lesson here is that “model returned empty string” and “model failed” are not the same thing, and you have to understand what each model family expects before you can interpret the output.</p> <h2 id="tool-calling-where-things-got-interesting">Tool-Calling: Where Things Got Interesting</h2> <p>Once the basic chat and code tests were passing, I added a tool-calling test. The prompt was “What’s the weather in Paris?” with a <code class="language-plaintext highlighter-rouge">get_weather</code> function schema attached to the request. A model that handles tool calling correctly should stop generating text and instead emit a structured <code class="language-plaintext highlighter-rouge">tool_calls</code> object pointing at <code class="language-plaintext highlighter-rouge">get_weather</code> with the right argument. A model that doesn’t understand the protocol either returns prose (“I don’t have access to weather data”), returns a JSON blob as plain text, or refuses the request entirely with an HTTP 400.</p> <p>The results split into three clear buckets. mistral, gemma4 (both sizes), qwen3:14b, qwen2.5:14b, and glm-4.7-flash all produced proper structured <code class="language-plaintext highlighter-rouge">tool_calls</code>. That is the expected behavior — the model uses the tool schema as intended.</p> <p>qwen2.5-coder:14b was the interesting failure. It returned what looked like a tool call, but as a raw JSON string embedded in the message content rather than as a structured <code class="language-plaintext highlighter-rouge">tool_calls</code> entry. The model clearly understood what was being asked; it just didn’t output it in the right format. A “coder” model is not necessarily a “tool-aware” model. They are different capabilities.</p> <p>llava:7b and phi4:14b both returned HTTP 400 on any request that included the <code class="language-plaintext highlighter-rouge">tools</code> field. Those models simply do not accept the parameter — the API rejects it before the model even sees the prompt. llava makes sense here: it is a vision model, not a chat/agent model. phi4 is less obvious.</p> <h2 id="mid-phase-additions">Mid-Phase Additions</h2> <p>While working through these tests I pulled in three more models that had come up in research as strong candidates for coding benchmarks: devstral:latest (22B, Devstral Small — Mistral’s coding-focused release), qwen3-coder:30b (~30B, Qwen’s coding-tuned variant), and gpt-oss:20b (~20B). All three were added before the formal scoring phase started.</p> <h2 id="the-baseline-table">The Baseline Table</h2> <p>Here is where every model stood after the initial phase — response times are wall-clock from the PowerShell script, rounded to the nearest second:</p> <table> <thead> <tr> <th>Model</th> <th>Size</th> <th>Chat</th> <th>Code</th> <th>Tool call</th> <th>Notes</th> </tr> </thead> <tbody> <tr> <td>mistral:latest</td> <td>7B</td> <td>3s</td> <td>1s</td> <td>proper</td> <td> </td> </tr> <tr> <td>llava:7b</td> <td>7B</td> <td>4s</td> <td>&lt;1s</td> <td>rejected</td> <td>Vision model</td> </tr> <tr> <td>gemma4:latest</td> <td>~12B</td> <td>6s</td> <td>1s</td> <td>proper</td> <td> </td> </tr> <tr> <td>qwen3:14b</td> <td>14B</td> <td>4s</td> <td>1s</td> <td>proper</td> <td>think=false required</td> </tr> <tr> <td>phi4:14b</td> <td>14B</td> <td>5s</td> <td>1s</td> <td>rejected</td> <td> </td> </tr> <tr> <td>qwen2.5:14b</td> <td>14B</td> <td>6s</td> <td>1s</td> <td>proper</td> <td> </td> </tr> <tr> <td>qwen2.5-coder:14b</td> <td>14B</td> <td>6s</td> <td>1s</td> <td>text (not structured)</td> <td>“coder” does not mean tool-aware</td> </tr> <tr> <td>gemma4:26b</td> <td>26B</td> <td>9s</td> <td>3s</td> <td>proper</td> <td>Partial CPU offload</td> </tr> <tr> <td>glm-4.7-flash</td> <td>30B MoE</td> <td>8s</td> <td>4s</td> <td>proper</td> <td> </td> </tr> <tr> <td>qwen3:30b</td> <td>30B</td> <td>14s</td> <td>8s</td> <td>proper</td> <td>Slowest in pool</td> </tr> </tbody> </table> <p>The latency numbers tell one story — size matters, mostly predictably. The tool-call column tells another: ten models, three different behaviors from the same input, and two of them would silently fail in any agentic loop that expected structured output.</p> <h2 id="what-works-actually-means">What “Works” Actually Means</h2> <p>The issue with this baseline is that “passes” hides a lot. A model that returns a tool call in the message content instead of the <code class="language-plaintext highlighter-rouge">tool_calls</code> field looks fine until your application tries to deserialize the response. A model that works at <code class="language-plaintext highlighter-rouge">num_predict=300</code> might silently truncate at <code class="language-plaintext highlighter-rouge">num_predict=100</code>. A model that answers “capital of France” correctly might write Python <code class="language-plaintext highlighter-rouge">is_prime()</code> that has an off-by-one error nobody noticed because nobody ran it.</p> <p>Everything in this phase was manual inspection. I was reading outputs and deciding they looked reasonable. That is not a test; that is a vibe check.</p> <p>The only way to actually know whether a model can write working code is to compile and run the code. Which meant building something more serious.</p> <hr /> <p><em>Next up: <a href="/erichexter/2026/05/28/local-llm-bench-part-2-building-the-harness/">Part 2</a> covers building the .NET 10 benchmark harness — including a scoring system that actually executes model-generated C# and runs the tests.</em></p> Back from the dead https://lostechies.com/erichexter/2026/05/23/back-from-the-dead/ Los Techies urn:uuid:ec34182e-8456-d04f-b2af-e2521985e73d Sat, 23 May 2026 12:00:00 +0000 Twelve years. My last post here was April 2014, and I closed it by promising “painstaking detail in the coming months” on what my team was building. Then I wrote exactly zero of those posts. Sorry about that. <p>Twelve years. My last post here was April 2014, and I closed it by promising “painstaking detail in the coming months” on what my team was building. Then I wrote exactly zero of those posts. Sorry about that.</p> <p>A lot has changed — starting with the site itself. When I last hit publish, lostechies.com was running on WordPress. Today it’s a Jekyll static site, hosted on GitHub Pages, and posting means committing a markdown file to <a href="https://github.com/lostechies/blog">lostechies/blog</a>. Which is honestly delightful. No login, no editor, no plugin upgrades. Write, commit, ship.</p> <p>In that spirit of bringing old things back to life: I also just revived <a href="https://github.com/erichexter/Should">Should</a>, the assertion library I built way back when. It’s been dragged forward into modern .NET and is usable again. More on that in a follow-up post.</p> <p>The bigger thing on my plate, though, is AI. I’ve been heads-down on agent development and agent frameworks — building them, breaking them, figuring out where the seams are. A few recent threads I’ve been pulling on over on LinkedIn: the <a href="https://www.linkedin.com/feed/update/urn:li:activity:7463782797799866368/">economics of AI software delivery</a>, <a href="https://www.linkedin.com/feed/update/urn:li:activity:7453535688387452928/">adversarial code reviews run by AI</a>, and <a href="https://www.linkedin.com/pulse/your-company-already-knows-everything-just-cant-remember-eric-hexter-kt3uc">why companies forget what they already know</a>. That’s most of what I want to write about going forward.</p> <p>I’m not going to promise a posting cadence — I learned my lesson in</p> <ol> <li>But if you stumbled back here from an old MvcContrib link or a 2012 SignalR post: welcome. The blog isn’t dead. It just needed a <code class="language-plaintext highlighter-rouge">git push</code>.</li> </ol> AutoMapper 16.1.1 Released https://www.jimmybogard.com/automapper-16-1-1-released/ Jimmy Bogard urn:uuid:849ecc30-fe8c-29b0-9f13-0f29b73a1cf3 Fri, 13 Mar 2026 15:58:13 +0000 <p>This release is a patch release to fix a thread deadlock and security issue. From the <a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.1.1?ref=jimmybogard.com" rel="noreferrer">release notes</a>:</p><h2 id="thread-deadlock">Thread Deadlock</h2><p>Thanks to <a href="https://github.com/t0m-4?ref=jimmybogard.com" rel="noreferrer">@t0m-4</a> for reporting <a href="https://github.com/LuckyPennySoftware/AutoMapper/issues/4612?ref=jimmybogard.com" rel="noreferrer">this issue</a>, which due to Microsoft deprecating some of the &quot;sync&quot; APIs for decryption, led to potential thread starvation and locking issues. The</p> <p>This release is a patch release to fix a thread deadlock and security issue. From the <a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.1.1?ref=jimmybogard.com" rel="noreferrer">release notes</a>:</p><h2 id="thread-deadlock">Thread Deadlock</h2><p>Thanks to <a href="https://github.com/t0m-4?ref=jimmybogard.com" rel="noreferrer">@t0m-4</a> for reporting <a href="https://github.com/LuckyPennySoftware/AutoMapper/issues/4612?ref=jimmybogard.com" rel="noreferrer">this issue</a>, which due to Microsoft deprecating some of the &quot;sync&quot; APIs for decryption, led to potential thread starvation and locking issues. The update still has to use a &quot;sync-over-async&quot; pattern, but does so in a much safer manner.</p><h2 id="security">Security</h2><p>We fixed an issue where certain cyclic or self-referential object graphs could trigger uncontrolled recursion during mapping, potentially resulting in stack exhaustion and denial of service.</p><p>Applications that process untrusted or attacker-controlled object graphs through affected mapping paths may be impacted.</p><p>Users should upgrade to this release.</p><p>Security advisory: <a href="https://github.com/LuckyPennySoftware/AutoMapper/security/advisories/GHSA-rvv3-g6hj-g44x?ref=jimmybogard.com" rel="noreferrer">GHSA-rvv3-g6hj-g44x</a></p><p>Thanks to <a href="https://github.com/bluefossa?ref=jimmybogard.com" rel="noreferrer">@bluefossa</a> for responsibly disclosing this issue.</p> AutoMapper 16.1 and MediatR 14.1 Released https://www.jimmybogard.com/automapper-16-1-and-mediatr-14-1-released/ Jimmy Bogard urn:uuid:2b05dd55-720e-3280-8b90-7aa2924b4642 Tue, 03 Mar 2026 21:50:55 +0000 <p>Today I released AutoMapper 16.1 and MediatR 14.1 (as part of now regular quarterly releases):</p><ul><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.1.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper Release Notes</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v14.1.0?ref=jimmybogard.com" rel="noreferrer">MediatR Release Notes</a></li></ul><p>AutoMapper added some interesting features, allowing for factories and conditions to use dependencies (previously only allowing <code>Func</code>-based callbacks. On the MediatR side, we added support for</p> <p>Today I released AutoMapper 16.1 and MediatR 14.1 (as part of now regular quarterly releases):</p><ul><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.1.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper Release Notes</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v14.1.0?ref=jimmybogard.com" rel="noreferrer">MediatR Release Notes</a></li></ul><p>AutoMapper added some interesting features, allowing for factories and conditions to use dependencies (previously only allowing <code>Func</code>-based callbacks. On the MediatR side, we added support for more interesting generic use cases in complex generic hierarchies as well as a number of bugs squashed.</p><p>You can get the latest drops on <a href="https://www.nuget.org/profiles/LuckyPennySoftware?ref=jimmybogard.com" rel="noreferrer">NuGet</a>. And a little celebration is in order - AutoMapper hit 1 billion downloads &#x1F973;</p><p>Enjoy!</p> AutoMapper 16.0.0 and MediatR 14.0.0 Released with .NET 10 Support https://www.jimmybogard.com/automapper-16-0-0-and-mediatr-14-0-0-released-with-net-10-support/ Jimmy Bogard urn:uuid:e709e6ee-ff61-9e9a-8a57-851e3a417be8 Wed, 03 Dec 2025 21:23:28 +0000 <p>With the release of .NET, we&apos;ve released updated packages of AutoMapper and MediatR targeting .NET 10 (and all supported versions of .NET and 4.x of .NET Framework).</p><p>From this release forward, we&apos;re aligning to major release cadences of .NET since this also means upping our</p> <p>With the release of .NET, we&apos;ve released updated packages of AutoMapper and MediatR targeting .NET 10 (and all supported versions of .NET and 4.x of .NET Framework).</p><p>From this release forward, we&apos;re aligning to major release cadences of .NET since this also means upping our dependency versions as well. Minor versions will still happen throughout the year but upping dependencies often has just as many issues as breaking API changes so we want to align with that cadence to make it easier for folks to understand when things are changing.</p><p>This release doesn&apos;t have any API changes from the previous minor release, but we did add <a href="https://learn.microsoft.com/en-us/nuget/create-packages/sign-a-package?ref=jimmybogard.com" rel="noreferrer">NuGet package signing</a>:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.jimmybogard.com/content/images/2025/12/image.png" class="kg-image" alt loading="lazy" width="1116" height="704" srcset="https://www.jimmybogard.com/content/images/size/w600/2025/12/image.png 600w, https://www.jimmybogard.com/content/images/size/w1000/2025/12/image.png 1000w, https://www.jimmybogard.com/content/images/2025/12/image.png 1116w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">NuGet package information with digital signatures</span></figcaption></figure><p>This shows that the AutoMapper (and MediatR) packages were published by <a href="https://luckypennysoftware.com/?ref=jimmybogard.com" rel="noreferrer">Lucky Penny Software</a> (my company) and not...Unlucky Penny Software? Getting this additional security verification is important for customers to know that this package comes from us and not some other entity. Getting verified for code signing certificates is a fairly involved process doing things like &quot;taking selfies with my passport&quot;.</p><p>Downloads and release notes below:</p><ul><li><a href="https://www.nuget.org/packages/automapper?ref=jimmybogard.com" rel="noreferrer">AutoMapper NuGet</a></li><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v16.0.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper v16.0.0 Release Notes</a></li><li><a href="https://www.nuget.org/packages/mediatr?ref=jimmybogard.com" rel="noreferrer">MediatR NuGet</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v14.0.0?ref=jimmybogard.com" rel="noreferrer">MediatR v14.00 Release Notes</a></li></ul><p>Enjoy!</p> AutoMapper 15.1 and MediatR 13.1 Released https://www.jimmybogard.com/automapper-15-1-and-mediatr-13-1-released/ Jimmy Bogard urn:uuid:fdcf91cf-e204-d239-232e-598ed9cc5714 Mon, 03 Nov 2025 22:06:16 +0000 <p>Starting a new policy of regular quarterly releases, today I pushed out new versions of AutoMapper and MediatR:</p><ul><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v15.1.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper 15.1.0 Release Notes</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v13.1.0?ref=jimmybogard.com" rel="noreferrer">MediatR 13.1.0 Release Notes</a></li></ul><p>The previous versions restored <code>netstandard2.0</code> support across the board. In these releases, I&apos;m including first-class support for</p> <p>Starting a new policy of regular quarterly releases, today I pushed out new versions of AutoMapper and MediatR:</p><ul><li><a href="https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v15.1.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper 15.1.0 Release Notes</a></li><li><a href="https://github.com/LuckyPennySoftware/MediatR/releases/tag/v13.1.0?ref=jimmybogard.com" rel="noreferrer">MediatR 13.1.0 Release Notes</a></li></ul><p>The previous versions restored <code>netstandard2.0</code> support across the board. In these releases, I&apos;m including first-class support for <code>net462</code> where I&apos;m building and testing on both *nix and Windows. We noticed that just including <code>netstandard2.0</code> as a target framework wasn&apos;t enough to really guarantee that the libraries would work against full .NET, so we now build and test on both.</p><p>There&apos;s some new features, enhancements, and bug fixes that you can view in the release notes above.</p><p>Enjoy!</p> AutoMapper and MediatR Roadmaps https://www.jimmybogard.com/automapper-and-mediatr-roadmaps/ Jimmy Bogard urn:uuid:e327d691-ad42-028b-c135-84474ca6add1 Tue, 08 Jul 2025 15:15:32 +0000 <p>One of my main goals of commercialization of AutoMapper and MediatR was being able to finally invest time in these projects where basically all new work stopped when I lost corporate sponsorship. I wanted to take some time to share where I&apos;d like to take these projects now</p> <p>One of my main goals of commercialization of AutoMapper and MediatR was being able to finally invest time in these projects where basically all new work stopped when I lost corporate sponsorship. I wanted to take some time to share where I&apos;d like to take these projects now that I have that sponsorship back.</p><h3 id="tracking-official-net-support">Tracking official .NET support</h3><p>Firstly, the latest releases bring back <code>netstandard2.0</code> support for both AutoMapper and MediatR which had dropped both years ago. MediatR was actually still on <code>net6.0</code> prior to this release which was already out of support for months.</p><p>It wasn&apos;t exactly easy, especially because of how much <code>net8.0</code> and <code>net9.0</code> have diverged from <code>netstandard2.0</code> not just in terms of APIs but C# language features, but having been part of several ASP.NET 4.x to ASP.NET Core migrations, having <code>netstandard2.0</code> support makes this transition quite a bit easier. In the past we&apos;d have to conditionally reference packages because there was no longer a common package version between say .NET 8 and .NET 4.8. That&apos;s something that I wish I had before that now I do.</p><h3 id="automapper-roadmap">AutoMapper Roadmap</h3><p>One of the biggest complaints I hear about AutoMapper is that it&apos;s hard to debug - you&apos;re trading compile-time errors for runtime exceptions. We spent a LOT of time baking in better exception handling into the expression trees generated (resulting in worse performance, but better diagnostics), but that isn&apos;t always enough.</p><p>The answer here is <strong>source generators</strong>, but I&apos;m not interested in merely copying other library&apos;s approaches. What I want to target is source generators that:</p><ul><li>Plug in to AutoMapper&apos;s rich extensibility model</li><li>Stay true to AutoMapper&apos;s <a href="https://www.jimmybogard.com/automappers-design-philosophy/" rel="noreferrer">design philosophy</a> </li><li>Support IQueryables (my favorite feature)</li><li>Track the features of AutoMapper&apos;s in-memory mapping</li><li>Support mapping validation (critical for any mapping tool)</li></ul><p>Debuggability is my main focus here, although obviously performance would be a secondary win. Source generators have come a LONG way since I first looked at them when they were first released, so I&apos;m excited to extend AutoMapper&apos;s functionality in this area.</p><p>This one is pretty big, so that&apos;s going to be my focus initially.</p><h3 id="mediatr-roadmap">MediatR Roadmap</h3><p>Some folks have asked or even pointed to other libraries that do source generation of basically a copy of MediatR&apos;s API. I am looking at that, but there&apos;s been quite a few things on MediatR&apos;s backlog that I want to look at first. Source generation in mediators I find a bit less interesting in real-world projects, outside of philosophical debates.</p><p>MediatR is commonly used in concert with <a href="https://www.jimmybogard.com/vertical-slice-architecture/" rel="noreferrer">Vertical Slice Architecture</a>, and a number of its features came out of using it in these scenarios (like behaviors). Today, a lot of features are tied in to the feature set of the stock Microsoft DI container. Unfortunately, features are only really added to that container if the ASP.NET Core team needs them. Even my PR to support generic constraints took like 5 years to merge in.</p><p>Moving away from relying on those DI features would mean I could do much more interesting things in the &quot;application use case pipeline&quot; that aren&apos;t possible with C#/DI alone, like:</p><ul><li><strong>Applying behaviors based on customized policies</strong></li><li><strong>Baking in support for result patterns</strong></li><li><strong>Direct support for application use cases</strong><ul><li>Blazor (sending a request from the client to a handler on the server)</li><li>Minimal APIs (scaffolding to separate API logic from application logic)</li><li>Domain events via notifications and EF/other ORMs</li></ul></li></ul><p>The idea of behaviors came from reviewing many production systems using MediatR and folding in that into first-class features. I am going to continue on this track.</p><p>What else are you interested in?</p> AutoMapper and MediatR Commercial Editions Launch Today https://www.jimmybogard.com/automapper-and-mediatr-commercial-editions-launch-today/ Jimmy Bogard urn:uuid:ea5d8cde-72ca-3b22-24cb-0be19ceb48af Wed, 02 Jul 2025 15:00:12 +0000 <p>Today I&apos;m excited to announce the official launch and release of the commercial editions of AutoMapper and MediatR. Both of these libraries have moved under their new corporate owner (me), <a href="https://luckypennysoftware.com/?ref=jimmybogard.com" rel="noreferrer">Lucky Penny Software</a>. I formed this company to house these projects separate from my consulting company, but it&</p> <p>Today I&apos;m excited to announce the official launch and release of the commercial editions of AutoMapper and MediatR. Both of these libraries have moved under their new corporate owner (me), <a href="https://luckypennysoftware.com/?ref=jimmybogard.com" rel="noreferrer">Lucky Penny Software</a>. I formed this company to house these projects separate from my consulting company, but it&apos;s just me there, I&apos;m the sole corporate overlord.</p><p>The GitHub repositories have transferred to the new GitHub organization (along with their ownership) here:</p><ul><li><a href="https://github.com/luckypennysoftware/automapper?ref=jimmybogard.com" rel="noreferrer">LuckyPennySoftware/AutoMapper</a></li><li><a href="https://github.com/luckypennysoftware/mediatr?ref=jimmybogard.com" rel="noreferrer">LuckyPennySoftware/MediatR</a></li></ul><p>With these, I&apos;ve launched new home pages for each library:</p><ul><li><a href="https://automapper.io/?ref=jimmybogard.com" rel="noreferrer">https://automapper.io</a></li><li><a href="https://mediatr.io/?ref=jimmybogard.com" rel="noreferrer">https://mediatr.io</a></li></ul><p>As well as a storefront site to purchase and manage licenses at:</p><ul><li><a href="https://luckypennysoftware.com/?ref=jimmybogard.com" rel="noreferrer">https://luckypennysoftware.com</a></li></ul><p>It&apos;s quite a bit to dig in to, so let&apos;s go over the details!</p><h3 id="whats-the-new-license">What&apos;s the new license?</h3><p>As <a href="https://www.jimmybogard.com/automapper-and-mediatr-licensing-update/" rel="noreferrer">discussed before</a>, I wanted to release these libraries under a <a href="https://github.com/LuckyPennySoftware/AutoMapper/blob/master/LICENSE.md?ref=jimmybogard.com" rel="noreferrer">dual-license model</a>:</p><ul><li><a href="https://opensource.org/license/rpl-1-5/?ref=jimmybogard.com" rel="noreferrer">Reciprocal Public License 1.5 (RPL1.5)</a></li><li><a href="https://luckypennysoftware.com/license?ref=jimmybogard.com" rel="noreferrer">Lucky Penny Software Commercial License</a></li></ul><p>It&apos;s a common dual-license model that many other OSS companies have chosen (MongoDB etc.) and had success with.</p><p>Under the commercial license, I&apos;ve created a <strong>tier-based licensing model</strong> based on <strong>team size</strong>. There are <strong>no individual per-seat licenses</strong>, only licensing based on the number of developers.</p><h3 id="how-much-will-it-cost">How much will it cost?</h3><p>With a tier-based pricing approach, I wanted a pricing model that scales with team size and allows for company growth without a lot of hassle. There are 3 paid tiers:</p><ul><li>Standard - <strong>1-10 developers</strong></li><li>Professional - <strong>11-50 developers</strong></li><li>Enterprise - <strong>Unlimited developers</strong></li></ul><p>Pricing is a <strong>subscription model</strong>, with both monthly and annual subscriptions (with a discount for annual subscriptions), as well as an option to <strong>bundle both libraries</strong> at a discount. You can find the details here (with all options), priced to your currency or in USD:</p><ul><li><a href="https://automapper.io/?ref=jimmybogard.com#pricing" rel="noreferrer">AutoMapper pricing</a></li><li><a href="https://mediatr.io/?ref=jimmybogard.com#pricing" rel="noreferrer">MediatR pricing</a></li></ul><p>You can also find the details of what subscription benefits you&apos;ll get at the links above, including:</p><ul><li>Private Discord channels</li><li>Priority support</li><li>Early access to new releases</li><li>Support for all currently supported versions of .NET Framework 4.x and .NET (<code>netstandard2.0</code>, <code>net8.0</code>, <code>net9.0</code>)</li><li>And more (as I build it)</li></ul><p>All subscription payments are managed through <a href="https://www.paddle.com/?ref=jimmybogard.com" rel="noreferrer">Paddle</a>, which supports...many different countries, currencies, and payment providers.</p><h3 id="do-you-have-free-licenses-for-insert-situation-here">Do you have free licenses for &lt;insert situation here&gt;? </h3><p>Yes! Besides the RPL license, I&apos;m also including a <strong>Community edition</strong> under the Commercial license that is <strong>free</strong> for:</p><ul><li>Companies and individuals <strong>under $5,000,000</strong> in gross annual revenue</li><li>Non-profits <strong>under $5,000,000</strong> in annual total budget (expenditure)</li><li>Educational/classroom use</li><li>Non-production environments</li></ul><p>You&apos;re still required to register for a license key, but this is only for auditing purposes.</p><h3 id="how-do-i-get-the-commercial-versions">How do I get the commercial versions?</h3><p>To make everyone&apos;s lives easier, these new major versions of AutoMapper and MediatR on NuGet are released under the new dual license agreement:</p><ul><li><a href="https://www.nuget.org/packages/AutoMapper/15.0.0?ref=jimmybogard.com" rel="noreferrer">AutoMapper v15.0</a></li><li><a href="https://www.nuget.org/packages/MediatR/13.0.0?ref=jimmybogard.com" rel="noreferrer">MediatR v13.0</a></li></ul><p>When you install these versions, you&apos;ll now be prompted for license acceptance. Once you obtain a license key, you&apos;ll be able to set the license key as:</p><pre><code class="language-csharp">services.AddAutoMapper(cfg =&gt; /* or AddMediatR */ cfg.LicenseKey = &quot;&lt;License key here&gt;&quot;; });</code></pre><p>I don&apos;t restrict usage of these products with a missing/invalid/expired license key, but you&apos;ll see some messages in your logs prompting you to supply a valid key.</p><h3 id="what-about-the-existing-versions">What about the existing versions?</h3><p>I&apos;ve created archived versions of the final releases of these two libraries:</p><ul><li><a href="https://github.com/automapper/automapper.archive?ref=jimmybogard.com" rel="noreferrer">AutoMapper/AutoMapper.Archive</a></li><li><a href="https://github.com/jbogard/mediatr.archive?ref=jimmybogard.com" rel="noreferrer">jbogard/MediatR.Archive</a></li></ul><p>Per those existing license agreements, you&apos;re free to fork, download, print out and read by the fireplace. Those archives will live on for anyone to use as they like.</p><p>If you&apos;re an existing user, you don&apos;t need to do anything. The existing NuGet packages (prior to the major versions listed above) are bound by the license agreements at the time of their release and will also live on.</p><h3 id="why-lucky-penny">Why Lucky Penny?</h3><p>Because she was my first dog! Although she&apos;s no longer with us anymore, I loved her spunk and her spirit and wanted to honor her memory with my company name (and logo). Here she is judging, always judging:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.jimmybogard.com/content/images/2025/07/DSC00167-copy.jpeg" class="kg-image" alt loading="lazy" width="1015" height="968" srcset="https://www.jimmybogard.com/content/images/size/w600/2025/07/DSC00167-copy.jpeg 600w, https://www.jimmybogard.com/content/images/size/w1000/2025/07/DSC00167-copy.jpeg 1000w, https://www.jimmybogard.com/content/images/2025/07/DSC00167-copy.jpeg 1015w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Penny the dog</span></figcaption></figure><p>I named her Penny because 1) she was found by the side of a busy highway miles from anywhere (lucky for both of us) and 2) her copper color. So, Lucky Penny Software!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.jimmybogard.com/content/images/2025/07/logo_wide.png" class="kg-image" alt loading="lazy" width="270" height="100"><figcaption><span style="white-space: pre-wrap;">Lucky Penny Software logo</span></figcaption></figure><p>It&apos;s been a long journey to get here but I&apos;m excited about what the future holds for these libraries that have amassed more than 1.1 billion downloads. Thanks everyone for your patience and support as I worked to launch!</p> AutoMapper and MediatR Licensing Update https://www.jimmybogard.com/automapper-and-mediatr-licensing-update/ Jimmy Bogard urn:uuid:d3c14382-1587-c8e6-e479-54c71c45f0ea Wed, 16 Apr 2025 05:15:28 +0000 <p>In my <a href="https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/" rel="noreferrer">last post</a>, I shared the news that I&apos;ve decided to take a commercialization route for AutoMapper and MediatR to ensure their long-term success. While that post was heavy on the motivation, it was intentionally light on the details. I did share that I wanted to be</p> <p>In my <a href="https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/" rel="noreferrer">last post</a>, I shared the news that I&apos;ve decided to take a commercialization route for AutoMapper and MediatR to ensure their long-term success. While that post was heavy on the motivation, it was intentionally light on the details. I did share that I wanted to be transparent on that process, and this post is part of that transparency.</p><p>There is a TON of information out there on possible models for sustainable open source, such as:</p><ul><li>Consulting services</li><li>Open core</li><li>Hosted services</li><li>Dual license</li><li>a dozen others</li></ul><p>Of course besides my previous situation, &quot;be fortunate enough to work at a place that values and directly sponsors your work.&quot; This is the easiest place to be, but for projects that reach some threshold of users/downloads/complexity, maintainers must rely on sponsorship in some form or fashion. And when that sponsorship goes away for whatever reason, well, here we are.</p><p>Of the many options available, the most viable option is to <strong>move AutoMapper and MediatR to a </strong><a href="https://en.wikipedia.org/wiki/Multi-licensing?ref=jimmybogard.com" rel="noreferrer"><strong>dual license model</strong></a>.<strong> </strong>This looks to be the best choice after carefully examining the options and consulting with many other OSS maintainers who have already made this journey.</p><h3 id="dual-licensing-model">Dual Licensing Model</h3><p>When I first started thinking about how I might go about this, I asked myself, &quot;who bears the most responsibility in ensuring the sustainability of the OSS projects on which they depend?&quot; which is a long winded way of saying &quot;who should pay?&quot; But another way of thinking of this is &quot;who should NOT pay?&quot; Looking at how others do this as well as how I want to approach it, <strong>I want to make these libraries free for</strong>:</p><ul><li>Developers using it in an OSS setting</li><li>Individuals/students/hobbyists (using AutoMapper for fun not profit)</li><li>Non-profit/charities (maybe not for fun but also not for profit)</li><li>Startups or small companies (below some revenue/funding threshold)</li><li>Non-commercial setting (this I&apos;m not sure is absolutely necessary with the other categories)</li><li>Non-production environments (instead of any trial period etc.)</li></ul><p>I don&apos;t know if this exact verbiage is what will be the end result, but this is my overall goal.</p><p>Then for who I&apos;m targeting for paid for-licenses, it&apos;s <strong>for-profit businesses using these libraries for commercial activities</strong>. Looking at my clients over the years who&apos;ve used my libraries, it&apos;s a mix of these free/commercial categories.</p><p>In terms of a model for commercial licensing, I want to ensure that paid licenses add value beyond &quot;I can download the license.&quot; This is the more fun part of this exercise for me, where I can try the things I never really could before without a more direct form of sponsorship/funding. I have a lot of ideas here, but nothing ready to share yet. If <em>you</em> have an idea of &quot;if my company paid for a license, what else would I want to have included?&quot; I would love to hear about it!</p><p>I am looking at a <strong>tiered license model</strong> but <strong>no per-seat licenses</strong>. I don&apos;t want to charge individual developers anything&#x2014;that seems like a pain for everyone involved and I&apos;m trying to keep things simple. &quot;A new developer gets onboarded and now we need a new license&quot; is too much for me to deal with and goes against the spirit of these libraries&#x2014;the benefit is to the entire team, regardless of the number of developers.</p><p>I don&apos;t know what those tiers will be exactly, I&apos;m figuring that out next. I do expect some blanket <strong>enterprise, site-wide licenses</strong> that hopefully makes everything simpler for everyone. I&apos;ve been on the other side of the table, getting licenses approved internally with clients, and I understand predictability and simplicity go a long way.</p><h3 id="thoughts-on-pricing">Thoughts on Pricing</h3><p>As for pricing, I don&apos;t have details yet, and probably won&apos;t until launch in the next couple months. Range-wise, it&apos;s hard to compare to other commercial or dual-licensed products out there, since I don&apos;t want to do any individual or per-seat license and that seems to be the norm. I am however keenly aware of how much tooling and library products cost as I have to pay for many of these myself.</p><p>But if I were to compare to the cost for a team of 10 or 50 or 100 for their IDEs, I would expect my commercial license price to be a fraction of that.</p><p>Thanks again to everyone that&apos;s reached out with kind words and support, and to the community for their patience while I figure things out.</p> AutoMapper and MediatR Going Commercial https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/ Jimmy Bogard urn:uuid:ea67b22b-0a25-2ca8-1045-cdc1bf9192f5 Wed, 02 Apr 2025 13:00:12 +0000 <p>Yes, another one of &quot;those posts&quot;. But tl;dr:</p><p><strong>In order to ensure the long-term sustainability of my OSS projects, I will be commercializing AutoMapper and MediatR.</strong></p><p>I did not post this on April 1st for obvious reasons. But first a little background on how I got to</p> <p>Yes, another one of &quot;those posts&quot;. But tl;dr:</p><p><strong>In order to ensure the long-term sustainability of my OSS projects, I will be commercializing AutoMapper and MediatR.</strong></p><p>I did not post this on April 1st for obvious reasons. But first a little background on how I got to this point.</p><h3 id="how-i-got-here">How I Got Here</h3><p>These two projects originated at my time at Headspring, a consulting company I worked at for over 12 years. About 5 years ago, in January 2020, I decided to strike off on my own and give solo consulting a try. Although it was a scary leap, it&apos;s been more rewarding than I could have possibly hoped for, in <em>almost</em> every area.</p><p>The area that it didn&apos;t work out well, and not at all intentionally, was OSS work:</p><figure class="kg-card kg-image-card"><img src="https://www.jimmybogard.com/content/images/2025/04/image-1.png" class="kg-image" alt loading="lazy" width="914" height="472" srcset="https://www.jimmybogard.com/content/images/size/w600/2025/04/image-1.png 600w, https://www.jimmybogard.com/content/images/2025/04/image-1.png 914w" sizes="(min-width: 720px) 720px"></figure><p>You can see exactly where my contributions cratered and flat-lined. And that&apos;s just commits&#x2014;issues, PRs, discussions, all my time dried up. This wasn&apos;t the intention but was a natural side effect of me focusing on my consulting business.</p><p>At Headspring, my time on OSS was directly encouraged and sponsored by them. I could use time between projects to invest back in existing OSS or new OSS, because it benefited the client, the company, and the employees (me and my coworkers).</p><p>With me leaving that company, and that company then selling to Accenture later that year, I had no direct major sponsor of my OSS work anymore. My free time was being spent growing and ensuring the success of my consulting company, which being solo, is...kinda important.</p><p>Taking time to see how things have been going on all fronts, I had a bit of a shock looking at my OSS work. I realized that model is not sustainable for the long-term success of these projects, which I still endorse and believe in. I need to be able to pay for my time to work on these projects, and get direct feedback from paying clients, like I had earlier at Headspring.</p><h3 id="what-will-this-look-like">What Will This Look Like?</h3><p>The short answer is &quot;I don&apos;t know exactly&quot;. I&apos;m working out those details now and will share them when I figure it out. I have lots of examples of what does and doesn&apos;t work well, at least from my perspective, as well as what I consider will work well for these projects.</p><p>Short term, nothing will change. I&apos;ll still be as (un)responsive on GitHub issues, and I just pushed out a couple releases of any existing work.</p><p>My goal is to be able to pay for the time to spend actually improving these projects, building out communities, helping more users, and in general, doing the things that people have asked me MANY times over the years that I should do, but I didn&apos;t, because it was not my job. OSS was/is/never will be a hobby for me. I want to change it to at least be part of my job and to fund real work.</p><p>I can&#x2019;t rely on donations, I don&apos;t want to make developers pay anything or do anything to punish/annoy them, and I certainly don&apos;t think it&apos;s Microsoft&apos;s job to &quot;pay me the money.&quot; Past that, I&apos;m still figuring it out.</p><h3 id="when-will-this-happen">When Will This Happen?</h3><p>I don&apos;t know, it&apos;s still just me that owns everything. It&apos;s still using my free time to sort it out, as my day job is still a consultant. But I plan to be open with this whole process. I&apos;m sure I&apos;ll surprise someone but the goal here is to be transparent.</p><p>Personally, I&apos;m both filled with excitement and dread&#x2014;doing these projects for so long has been incredibly rewarding, especially as this is code that came directly out of many, many long-lived production-deployed projects at Headspring. But I don&apos;t want these projects to wither and die on the vine, I want them to grow and evolve and thrive. But not just these projects&#x2014;I want ALL my OSS projects (Respawn etc.) to thrive. This is how it needs to happen.</p><h3 id="final-thanks">Final Thanks</h3><p>Thanks to all that have contributed over the years, and especially to <a href="https://www.linkedin.com/in/lbargaoanu/?ref=jimmybogard.com" rel="noreferrer">Lucian Bargaoanu</a> who really helped pick up the torch with AutoMapper after I more or less fell off the map. Also thanks to my GitHub sponsors, as many a pint has been purchased with your generous support. And finally thanks to the community, I never hoped anything I built would help anyone beyond my clients, coworkers, and company, but it&apos;s always nice to hear that it has.</p> MediatR 12.5.0 Released https://www.jimmybogard.com/mediatr-12-5-0-released/ Jimmy Bogard urn:uuid:fc72edcb-d39a-2519-28e0-208cbad24a55 Tue, 01 Apr 2025 18:50:06 +0000 <p>I pushed out MediatR 12.5 today:</p><ul><li><a href="https://github.com/jbogard/MediatR/releases/tag/v12.5.0?ref=jimmybogard.com" rel="noreferrer">Release Notes</a></li><li><a href="https://www.nuget.org/packages/MediatR?ref=jimmybogard.com" rel="noreferrer">NuGet</a></li></ul><p>This is mainly a regular minor release with a couple extra interesting features:</p><ul><li><a href="https://github.com/jbogard/MediatR/pull/1065?ref=jimmybogard.com" rel="noreferrer">Adding convenience method to register open behaviors</a></li><li>Better cancellation token support (it&apos;s passed now everywhere including behaviors)</li></ul><p>And some other cleanup items as well. Enjoy!</p> <p>I pushed out MediatR 12.5 today:</p><ul><li><a href="https://github.com/jbogard/MediatR/releases/tag/v12.5.0?ref=jimmybogard.com" rel="noreferrer">Release Notes</a></li><li><a href="https://www.nuget.org/packages/MediatR?ref=jimmybogard.com" rel="noreferrer">NuGet</a></li></ul><p>This is mainly a regular minor release with a couple extra interesting features:</p><ul><li><a href="https://github.com/jbogard/MediatR/pull/1065?ref=jimmybogard.com" rel="noreferrer">Adding convenience method to register open behaviors</a></li><li>Better cancellation token support (it&apos;s passed now everywhere including behaviors)</li></ul><p>And some other cleanup items as well. Enjoy!</p> AutoMapper 14.0 Released https://www.jimmybogard.com/automapper-14-0-released/ Jimmy Bogard urn:uuid:064fc7c2-f054-0fef-5997-60e150011a79 Wed, 19 Feb 2025 13:41:24 +0000 <p>I pushed out version 14.0 (!) of AutoMapper over the weekend:</p><ul><li><a href="https://github.com/AutoMapper/AutoMapper/releases/tag/v14.0.0?ref=jimmybogard.com" rel="noreferrer">Release notes</a></li><li><a href="https://www.nuget.org/packages/automapper/?ref=jimmybogard.com" rel="noreferrer">NuGet</a></li></ul><p>This release targets .NET 8 (up from .NET 6 from the previous release). It&apos;s mainly a bug fix release, with some quality-of-life improvements in configuration validation where we gather up all the possible validation</p> <p>I pushed out version 14.0 (!) of AutoMapper over the weekend:</p><ul><li><a href="https://github.com/AutoMapper/AutoMapper/releases/tag/v14.0.0?ref=jimmybogard.com" rel="noreferrer">Release notes</a></li><li><a href="https://www.nuget.org/packages/automapper/?ref=jimmybogard.com" rel="noreferrer">NuGet</a></li></ul><p>This release targets .NET 8 (up from .NET 6 from the previous release). It&apos;s mainly a bug fix release, with some quality-of-life improvements in configuration validation where we gather up all the possible validation errors before reporting them in an aggregate exception.</p><p>Enjoy!</p> Integrating the Particular Service Platform with Aspire https://www.jimmybogard.com/integrating-the-particular-service-platform-with-aspire/ Jimmy Bogard urn:uuid:cfec7702-7b24-2b56-5170-6ecd6a62f692 Tue, 24 Sep 2024 22:48:57 +0000 <p>I&apos;ve been playing around with <a href="https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview?ref=jimmybogard.com" rel="noreferrer">Aspire</a> for a bit mainly to understand &quot;is this a thing I should care about?&quot; and part of what I wanted to do is take a complex &quot;hello world&quot; distributed system and convert it to Aspire. Along the way,</p> <p>I&apos;ve been playing around with <a href="https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview?ref=jimmybogard.com" rel="noreferrer">Aspire</a> for a bit mainly to understand &quot;is this a thing I should care about?&quot; and part of what I wanted to do is take a complex &quot;hello world&quot; distributed system and convert it to Aspire. Along the way, Particular Software also released container support for their <a href="https://particular.net/service-platform?ref=jimmybogard.com" rel="noreferrer">Service Platform</a>, so it also seemed like a good opportunity to try it out.</p><p>I&apos;ll follow up in another post about Aspire impressions, but the NServiceBus part was actually relatively simple. Many Aspire integrations have some kind of 1st-party support where you can do things like:</p><pre><code class="language-csharp">var rmqPassword = builder.AddParameter(&quot;messaging-password&quot;); var dbPassword = builder.AddParameter(&quot;db-password&quot;); var broker = builder.AddRabbitMQ(name: &quot;broker&quot;, password: rmqPassword, port: 5672) .WithDataVolume() .WithManagementPlugin() .WithEndpoint(&quot;management&quot;, e =&gt; e.Port = 15672) .WithHealthCheck(); var mongo = builder.AddMongoDB(&quot;mongo&quot;); var sql = builder.AddSqlServer(&quot;sql&quot;, password: dbPassword) .WithHealthCheck() .WithDataVolume() .AddDatabase(&quot;sqldata&quot;);</code></pre><p>And now my system has RabbitMQ, MongoDB, and SQL Server up and running in containers. There&apos;s a lot of stock configuration going on behind <code>AddSqlServer</code> and similar methods but we don&apos;t <em>have</em> to use those convenience methods if we don&apos;t want to.</p><p>The overall Service Platform architecture looks something like:</p><figure class="kg-card kg-image-card"><img src="https://www.jimmybogard.com/content/images/2024/09/image-1.png" class="kg-image" alt loading="lazy" width="2000" height="1136" srcset="https://www.jimmybogard.com/content/images/size/w600/2024/09/image-1.png 600w, https://www.jimmybogard.com/content/images/size/w1000/2024/09/image-1.png 1000w, https://www.jimmybogard.com/content/images/size/w1600/2024/09/image-1.png 1600w, https://www.jimmybogard.com/content/images/2024/09/image-1.png 2352w" sizes="(min-width: 720px) 720px"></figure><p>The &quot;instances&quot; here are running containers that we need to configure in Aspire. On top of that, we might also want to have Service Pulse (another container) and Service Insight (a Windows-only WPF app) running, and these all require extra configuration. Also, the Error and Audit instances use RavenDB as their backing store but Particular also has an image there. The <a href="https://hub.docker.com/r/particular/servicecontrol?ref=jimmybogard.com" rel="noreferrer">Docker Hub site</a> has links to docs on both the instance and containers.</p><p>First up, we need to provide our license to the running containers as raw text in an environment variable, so we&apos;ll just read our license (this is just for local development):</p><pre><code class="language-csharp">var license = File.ReadAllText( Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), &quot;ParticularSoftware&quot;, &quot;license.xml&quot;)); </code></pre><p>Next, we need our RavenDB instance. There&apos;s a special image from Particular, so we&apos;ll use the <code>AddContainer</code> method to add our custom image to our Aspire distributed application:</p><pre><code class="language-csharp">builder .AddContainer(&quot;servicecontroldb&quot;, &quot;particular/servicecontrol-ravendb&quot;, &quot;latest&quot;) .WithBindMount(&quot;AppHost-servicecontroldb-data&quot;, &quot;/opt/RavenDB/Server/RavenData&quot;) .WithEndpoint(8080, 8080); </code></pre><p>The container docs say that we must mount a persistent volume to that path, so we use the <code>WithBindMount</code> method to mount the volume following the <a href="https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/persist-data-volumes?ref=jimmybogard.com#understand-volumes" rel="noreferrer">Aspire docs</a>.</p><p>Next up are the Particular containers!</p><h3 id="setting-up-the-service-control-error-instance">Setting up the Service Control Error instance</h3><p>From the <a href="https://docs.particular.net/servicecontrol/servicecontrol-instances/deployment/containers?ref=jimmybogard.com" rel="noreferrer">Particular docs</a>, we see that we need to supply configuration for:</p><ul><li>Transport type (RabbitMQ, Azure Service Bus, etc.)</li><li>Connection string to the transport</li><li>Connection string to the Raven DB instance</li><li>Audit instance URLs</li><li>License</li></ul><p>Plus port mapping. Pretty quickly I ran into a few challenges:</p><ul><li>The Service Control image can start before RabbitMQ is &quot;ready&quot;, resulting in connection failures</li><li>Service Insight, the WPF app, is Windows only so I need to connect to Service Control from a VM</li></ul><p>The base configuration is fairly straightforward, we specify the container and image, with environment variables:</p><pre><code class="language-csharp">builder .AddContainer(&quot;servicecontrol&quot;, &quot;particular/servicecontrol&quot;) .WithEnvironment(&quot;TransportType&quot;, &quot;RabbitMQ.QuorumConventionalRouting&quot;) .WithEnvironment(&quot;ConnectionString&quot;, &quot;host=host.docker.internal&quot;) .WithEnvironment(&quot;RavenDB_ConnectionString&quot;, &quot;http://host.docker.internal:8080&quot;) .WithEnvironment(&quot;RemoteInstances&quot;, &quot;[{\&quot;api_uri\&quot;:\&quot;http://host.docker.internal:44444/api\&quot;}]&quot;) .WithEnvironment(&quot;PARTICULARSOFTWARE_LICENSE&quot;, license) .WithArgs(&quot;--setup-and-run&quot;) </code></pre><p>But the other two challenges are a bit harder to deal with. There is no built-in way in Aspire to &quot;wait&quot; for other resources to start. This isn&apos;t new to Aspire - in the past we had to write custom hooks in Docker Compose to wait for our dependencies&apos; health checks to come back. The extensibility is there to do such a thing, so I found an <a href="https://nikiforovall.github.io/dotnet/aspire/2024/06/28/startup-dependencies-aspire.html?ref=jimmybogard.com" rel="noreferrer">extension to do just that</a>.</p><p>The second problem was...a long slog to figure out. It&apos;s possible to have a Parallels VM be able to communicate with Docker containers <a href="https://samestuffdifferentday.net/2024/08/02/working-in-parallels-and-docker-on-host/?ref=jimmybogard.com" rel="noreferrer">running in the Mac host</a>. However, I could <strong>not</strong> get this to work with Aspire. After doing side-by-side comparisons between container manifests running inside/outside of Aspire, I found the culprit:</p><pre><code class="language-diff">&quot;PortBindings&quot;: { &quot;8080/tcp&quot;: [ { - &quot;HostIp&quot;: &quot;&quot;, + &quot;HostIp&quot;: &quot;127.0.0.1&quot;, &quot;HostPort&quot;: &quot;8000&quot; } ] }, </code></pre><p>With the Docker CLI, doing <code>-p 8080:8000</code> does not set the host IP. Aspire does however, which means I can only access this container via <code>localhost</code>. Not ideal because my Windows VM is definitely not able to access that. Instead of using <code>WithEndpoint</code> or similar, I have to drop down to container runtime args:</p><pre><code class="language-csharp">.WithContainerRuntimeArgs(&quot;-p&quot;, &quot;33333:33333&quot;) .WaitFor(rabbitMqResource); </code></pre><p>Now my Service Control instance is up and running!</p><h3 id="setting-up-service-control-audit-monitoring-and-service-pulse">Setting up Service Control Audit, Monitoring, and Service Pulse</h3><p>Following our previous example, we can finish out our configuration for the other container instances:</p><pre><code class="language-csharp">builder .AddContainer(&quot;servicecontrolaudit&quot;, &quot;particular/servicecontrol-audit&quot;) .WithEnvironment(&quot;TransportType&quot;, &quot;RabbitMQ.QuorumConventionalRouting&quot;) .WithEnvironment(&quot;ConnectionString&quot;, &quot;host=host.docker.internal&quot;) .WithEnvironment(&quot;RavenDB_ConnectionString&quot;, &quot;http://host.docker.internal:8080&quot;) .WithEnvironment(&quot;PARTICULARSOFTWARE_LICENSE&quot;, license) .WithArgs(&quot;--setup-and-run&quot;) .WithEndpoint(44444, 44444) .WaitFor(rabbitMqResource); builder .AddContainer(&quot;servicecontrolmonitoring&quot;, &quot;particular/servicecontrol-monitoring&quot;) .WithEnvironment(&quot;TransportType&quot;, &quot;RabbitMQ.QuorumConventionalRouting&quot;) .WithEnvironment(&quot;ConnectionString&quot;, &quot;host=host.docker.internal&quot;) .WithEnvironment(&quot;PARTICULARSOFTWARE_LICENSE&quot;, license) .WithArgs(&quot;--setup-and-run&quot;) .WithEndpoint(33633, 33633) .WaitFor(rabbitMqResource); builder .AddContainer(&quot;servicepulse&quot;, &quot;particular/servicepulse&quot;) .WithEnvironment(&quot;SERVICECONTROL_URL&quot;, &quot;http://host.docker.internal:33333&quot;) .WithEnvironment(&quot;MONITORING_URL&quot;, &quot;http://host.docker.internal:33633&quot;) .WithEnvironment(&quot;PARTICULARSOFTWARE_LICENSE&quot;, license) .WithEndpoint(9090, 9090) .WaitFor(rabbitMqResource); </code></pre><p>With all this in place in my Service Pulse instance is up and running:</p><figure class="kg-card kg-image-card"><img src="https://www.jimmybogard.com/content/images/2024/09/image-2.png" class="kg-image" alt loading="lazy" width="1964" height="886" srcset="https://www.jimmybogard.com/content/images/size/w600/2024/09/image-2.png 600w, https://www.jimmybogard.com/content/images/size/w1000/2024/09/image-2.png 1000w, https://www.jimmybogard.com/content/images/size/w1600/2024/09/image-2.png 1600w, https://www.jimmybogard.com/content/images/2024/09/image-2.png 1964w" sizes="(min-width: 720px) 720px"></figure><p>And on the Service Insight side, I had to do the Parallels trick of using my hosts file to create a special &quot;localhost.mac&quot; entry to point to the Mac host:</p><pre><code>10.211.55.2 localhost.mac</code></pre><p>With this in place, I can configure Service Insight in Windows to connect to the Docker Service Pulse instance running in Docker on the Mac:</p><figure class="kg-card kg-image-card"><img src="https://www.jimmybogard.com/content/images/2024/09/image-3.png" class="kg-image" alt loading="lazy" width="662" height="264" srcset="https://www.jimmybogard.com/content/images/size/w600/2024/09/image-3.png 600w, https://www.jimmybogard.com/content/images/2024/09/image-3.png 662w"></figure><p>All my NServiceBus messages and traces now show up just fine:</p><figure class="kg-card kg-image-card"><img src="https://www.jimmybogard.com/content/images/2024/09/image-4.png" class="kg-image" alt loading="lazy" width="2000" height="1483" srcset="https://www.jimmybogard.com/content/images/size/w600/2024/09/image-4.png 600w, https://www.jimmybogard.com/content/images/size/w1000/2024/09/image-4.png 1000w, https://www.jimmybogard.com/content/images/size/w1600/2024/09/image-4.png 1600w, https://www.jimmybogard.com/content/images/2024/09/image-4.png 2266w" sizes="(min-width: 720px) 720px"></figure><p>Most of the work I had to do was not really Aspire-related, but just configuring Aspire to pass in the appropriate configuration to the containers. You can find the full code to my configuration here:</p><p><a href="https://github.com/jbogard/nsb-diagnostics-poc?ref=jimmybogard.com" rel="noreferrer">Code Example</a></p><p>Enjoy!</p> Tales from the .NET Migration Trenches - Turning Off the Lights https://www.jimmybogard.com/tales-from-the-net-migration-trenches-turning-off-the-lights/ Jimmy Bogard urn:uuid:ccf8129e-f722-7635-4846-e2324d7a66fa Thu, 05 Sep 2024 15:25:16 +0000 <p>Posts in this series:</p><ul><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches/" rel="noreferrer">Intro</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-catalog" rel="noreferrer">Cataloging</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-empty-proxy" rel="noreferrer">Empty Proxy</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-shared-library" rel="noreferrer">Shared Library</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-our-first-controller" rel="noreferrer">Our First Controller</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-migrating-business-logic" rel="noreferrer">Migrating Initial Business Logic</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-our-first-views" rel="noreferrer">Our First Views</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-session-state/" rel="noreferrer">Session State</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-hangfire/" rel="noreferrer">Hangfire</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-authentication/" rel="noreferrer">Authentication</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-middleware/" rel="noreferrer">Middleware</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-turning-off-the-lights/" rel="noreferrer">Turning Off the Lights</a></li></ul><p>In the last post, we looked at migrating our middleware, which we tackle in an as-needed basis. When a controller needs</p> <p>Posts in this series:</p><ul><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches/" rel="noreferrer">Intro</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-catalog" rel="noreferrer">Cataloging</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-empty-proxy" rel="noreferrer">Empty Proxy</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-shared-library" rel="noreferrer">Shared Library</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-our-first-controller" rel="noreferrer">Our First Controller</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-migrating-business-logic" rel="noreferrer">Migrating Initial Business Logic</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-our-first-views" rel="noreferrer">Our First Views</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-session-state/" rel="noreferrer">Session State</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-hangfire/" rel="noreferrer">Hangfire</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-authentication/" rel="noreferrer">Authentication</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-middleware/" rel="noreferrer">Middleware</a></li><li><a href="https://www.jimmybogard.com/tales-from-the-net-migration-trenches-turning-off-the-lights/" rel="noreferrer">Turning Off the Lights</a></li></ul><p>In the last post, we looked at migrating our middleware, which we tackle in an as-needed basis. When a controller needs middleware to be migrated, we migrate that middleware over. If the entire app needs the middleware, it needs to come rather early.</p><p>Once we migrate much of our middleware over, it becomes much less work to incrementally migrate individual controllers and their subsequent actions/pages over. I won&apos;t go into deep detail into this part - mostly it&apos;s fixing namespaces, adjusting features (such as converting child actions into view components), but it can go <em>quite</em> fast. On recent teams I was working with, we migrated easily a dozen controllers a week amongst 3-4 developers. At this point, the bottleneck wasn&apos;t the conversion, but testing to make sure the pages still worked correctly</p><p>It&apos;s essentially testing the entire application, one page at a time, so hopefully you&apos;ve got some regression tests in some form or fashion. I&apos;m not skipping the incremental controller migration because it&apos;s not interesting - it&apos;s just because our teams really didn&apos;t encounter many challenges there. There will be <em>something</em> that comes up, there always is, but just the controller/action/view part is not too terrible.</p><p>But in this post I wanted to focus on getting to the end - what do we do once we&apos;ve migrated everything but authentication? When there&apos;s just one controller left, we&apos;re now OK to proceed with migrating the last pieces over and &quot;turning off the lights&quot; on the .NET 4.x application.</p><h3 id="migrating-last-features">Migrating Last Features</h3><p>The last (or next-to-last) migration typically:</p><ul><li>Migrates the last controller, usually authentication</li><li>Turns off proxying and all remote app features</li></ul><p>You don&apos;t necessarily need to split this into two separate units of work/deployments, as once you&apos;ve migrated the last set of requests you can migrate all final features from the .NET Framework application. If the last controller is authentication, we&apos;ll also need to remove remote authentication. Our current web adapter configuration before final migration is:</p><pre><code class="language-csharp">builder.Services.AddSystemWebAdapters() .AddJsonSessionSerializer(options =&gt; { options.RegisterKey&lt;string&gt;(&quot;FavoriteInstructor&quot;); }) .AddRemoteAppClient(options =&gt; { // Provide the URL for the remote app that has enabled session querying options.RemoteAppUrl = new(builder.Configuration[&quot;ProxyTo&quot;]); // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session options.ApiKey = builder.Configuration[&quot;RemoteAppApiKey&quot;]; }) .AddAuthenticationClient(true) .AddSessionClient(); builder.Services.AddHttpForwarder();</code></pre><p>With middleware:</p><pre><code class="language-csharp">app.UseSystemWebAdapters(); app.MapDefaultControllerRoute(); app.MapForwarder(&quot;/{**catch-all}&quot;, app.Configuration[&quot;ProxyTo&quot;]).Add( static builder =&gt; ((RouteEndpointBuilder)builder).Order = int.MaxValue);</code></pre><p>Along with migrating the authentication piece and all related middleware, we&apos;ll remove the above from our application startup, as well as the package references to all the proxy and System.WebAdapters packages. Once that&apos;s complete, our .NET application should now handle <em>all</em> requests. There might still be a few extra features to enable in .NET 8, such as Session:</p><pre><code class="language-csharp">builder.Services.AddSession(); // later app.UseSession();</code></pre><p>With all that complete, our .NET 8 application should now serve all requests and host all features needed to run our entire system.</p><h3 id="turning-off-the-lights">Turning off the lights</h3><p>While our .NET 8 application may now be &quot;complete&quot;, we&apos;re not quite done yet. In my typical last phase we will:</p><ul><li>Deploy the completed .NET 8 application to production</li><li>Monitor for any errors and any activity from the .NET 4.8 application</li><li>Adjust our .NET 8 application as necessary</li></ul><p>If we don&apos;t see any issues, then the final <em>final</em> cleanup is:</p><ul><li>Remove all .NET 4.8 code from the repository</li><li>Remove any shims to bridge from .NET 8 to .NET 4.8</li><li>Remove all .NET 4.8 application pipelines and deployments</li><li>Remove all .NET 4.8 production resources</li></ul><p>And we should end with something like:</p><figure class="kg-card kg-image-card"><a href="https://x.com/jbogard/status/1813667695289933872?ref=jimmybogard.com"><img src="https://www.jimmybogard.com/content/images/2024/09/image.png" class="kg-image" alt loading="lazy" width="1172" height="664" srcset="https://www.jimmybogard.com/content/images/size/w600/2024/09/image.png 600w, https://www.jimmybogard.com/content/images/size/w1000/2024/09/image.png 1000w, https://www.jimmybogard.com/content/images/2024/09/image.png 1172w" sizes="(min-width: 720px) 720px"></a></figure><p>So what&apos;s next? There&apos;s still probably quite a bit to do to &quot;.NET-8-ify&quot; our existing system - all those architectural improvements we skipped in order to fast track migration. But most important - celebrate!</p> Upcoming Training on DDD with Vertical Slice Architecture in Munich https://www.jimmybogard.com/upcoming-training-on-ddd-with-vertical-slice-architecture-in-munich/ Jimmy Bogard urn:uuid:dc225ab4-6a2c-1ffc-01c6-ad5193bb0ff1 Wed, 28 Aug 2024 10:28:04 +0000 <p>I&apos;ve got another training event coming up focusing on Domain-Driven Design with Vertical Slice Architecture in Munich on October 21-23rd.</p><p>A little different than the previous times I&apos;ve given this course is an option for either a 2-day or 3-day version. I had received feedback that</p> <p>I&apos;ve got another training event coming up focusing on Domain-Driven Design with Vertical Slice Architecture in Munich on October 21-23rd.</p><p>A little different than the previous times I&apos;ve given this course is an option for either a 2-day or 3-day version. I had received feedback that folks were also interested in larger-scale design concepts such as bounded contexts, messaging, integration patterns, microservices, and modular monoliths. So I&apos;ve included a 3rd day that covers these topics, where we look at encapsulation and cohesion at larger and larger scopes.</p><p>We&apos;ll cover:</p><ul><li>Refactoring an existing system to leverage Vertical Slice Architecture</li><li>Applying Domain-Driven Design techniques to model complex business needs</li><li>Communication between slices</li><li>Exploring Validation and Testing (and other cross-cutting concerns) using Vertical Slice Architecture</li><li>Examining various design patterns, code smells, and refactoring techniques</li><li>Implementing the Vertical Slice Architectural pattern in various enterprise application scenarios (minimal APIs, Blazor, Web APIs, etc.)</li></ul><p>And on the final day:</p><ul><li>Service boundaries and bounded contexts</li><li>Communication between bounded contexts</li><li>Microservices and modular monoliths</li><li>Studying distributed systems patterns, tools, and libraries such as NServiceBus</li></ul><p>The course pulls together my experiences building such systems for nearly 20 years now. And if you can&apos;t make the course during the day, I&apos;m also hosting a networking event during the evening where you can meet myself and the other attendees and ask me questions. I hope to see you there!</p><p><a href="https://my.weezevent.com/domain-driven-design-with-vertical-slice-architecture-1?ref=jimmybogard.com" rel="noreferrer">Register Now</a></p> SSH on WSL http://aspiringcraftsman.com/2022/07/01/ssh-on-wsl.html Aspiring Craftsman urn:uuid:3b8d779e-bac3-a739-db05-ece712be54e3 Fri, 01 Jul 2022 08:00:00 +0000 <p style="text-align: center"> <a href="/wp-content/uploads/2022/07/01/ssh.png"><img title="SSH" alt="" src="/wp-content/uploads/2022/07/01/ssh.png" width="640" height="480" sizes="(max-width: 640px) 100vw, 640px" /></a> </p> <p>I recently set up a Windows machine to allow me to ssh into its WSL network from another box. I found a couple of useful guides <a href="https://www.hanselman.com/blog/how-to-ssh-into-wsl2-on-windows-10-from-an-external-machine">here</a> and <a href="https://www.hanselman.com/blog/how-to-ssh-into-wsl2-on-windows-10-from-an-external-machine">here</a>, but still ran into a few snags along the way, so thought I’d publish my configuration in case it might be useful to someone else (or perhaps even myself) in the future. Here are my steps:</p> <h1 id="step-1-install-openssh-in-wsl">Step 1: Install OpenSSH in WSL</h1> <p>Our first step will be to ensure openssh-server is installed. If not, issue the following and follow the prompts:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>openssh-server </code></pre></div></div> <h1 id="step-2-configure-sshd">Step 2: Configure SSHD</h1> <p>Edit the file /etc/ssh/sshd_config to allow the desired users or groups. You’ll need to edit this with root access:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>vi sshd_config </code></pre></div></div> <p>To allow specific users, you can add the following with a list of users where <user1> is replaced with the desired user:</user1></p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AllowUsers &lt;user1&gt; &lt;user2&gt; &lt;userN&gt; </code></pre></div></div> <p>To allow all users in one or more groups, add the following:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AllowGroups &lt;group1&gt; &lt;group2&gt; &lt;groupN&gt; </code></pre></div></div> <p>Note that these are users and groups known to WSL, not your windows users and groups (i.e. /etc/password, /etc/group).</p> <h1 id="step-3-setup-startup-script">Step 3: Setup Startup Script</h1> <p>Create a new file named startup-ssh.sh in /usr/local/bin with the following contents:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span> <span class="nv">WSL_IP</span><span class="o">=</span><span class="si">$(</span>ip addr show eth0| <span class="nb">grep</span> <span class="nt">-oP</span> <span class="s1">'(?&lt;=inet\s)\d+(\.\d+){3}'</span><span class="si">)</span> <span class="nv">NETSH_CMD</span><span class="o">=</span>/mnt/c/Windows/System32/netsh.exe <span class="nv">SSL_PORT</span><span class="o">=</span>22 <span class="nv">FIREWALL_RULE_NAME</span><span class="o">=</span><span class="s2">"SSH Port </span><span class="k">${</span><span class="nv">SSL_PORT</span><span class="k">}</span><span class="s2">"</span> <span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"Resetting port proxy settings ... "</span> <span class="k">${</span><span class="nv">NETSH_CMD</span><span class="k">}</span> interface portproxy reset all 2&gt;&amp;1 1&gt;/dev/null <span class="o">[</span> <span class="nv">$?</span> <span class="o">==</span> 0 <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"OK"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"Error"</span> <span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"Forwarding port </span><span class="k">${</span><span class="nv">SSL_PORT</span><span class="k">}</span><span class="s2"> to </span><span class="k">${</span><span class="nv">WSL_IP</span><span class="k">}</span><span class="s2"> ... "</span> <span class="k">${</span><span class="nv">NETSH_CMD</span><span class="k">}</span> interface portproxy add v4tov4 <span class="nv">listenaddress</span><span class="o">=</span>0.0.0.0 <span class="nv">listenport</span><span class="o">=</span><span class="k">${</span><span class="nv">SSL_PORT</span><span class="k">}</span> <span class="nv">connectaddress</span><span class="o">=</span><span class="k">${</span><span class="nv">WSL_IP</span><span class="k">}</span> <span class="nv">connectport</span><span class="o">=</span><span class="k">${</span><span class="nv">SSL_PORT</span><span class="k">}</span> 2&gt;&amp;1 1&gt;/dev/null <span class="o">[</span> <span class="nv">$?</span> <span class="o">==</span> 0 <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"OK"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"Error"</span> <span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"Adding firewall rule if not present ... "</span> <span class="k">if</span> <span class="k">${</span><span class="nv">NETSH_CMD</span><span class="k">}</span> advfirewall firewall show rule <span class="nv">name</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">FIREWALL_RULE_NAME</span><span class="k">}</span><span class="s2">"</span> 2&gt;&amp;1 1&gt;/dev/null <span class="k">then </span><span class="nb">echo</span> <span class="s2">"OK"</span> <span class="k">else</span> <span class="k">${</span><span class="nv">NETSH_CMD</span><span class="k">}</span> advfirewall firewall add rule <span class="nv">name</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">FIREWALL_RULE_NAME</span><span class="k">}</span><span class="s2">"</span> <span class="nb">dir</span><span class="o">=</span><span class="k">in </span><span class="nv">action</span><span class="o">=</span>allow <span class="nv">protocol</span><span class="o">=</span>TCP <span class="nv">localport</span><span class="o">=</span><span class="k">${</span><span class="nv">SSL_PORT</span><span class="k">}</span> 2&gt;&amp;1 1&gt;/dev/null <span class="o">[</span> <span class="nv">$?</span> <span class="o">==</span> 0 <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"OK"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"Error"</span> <span class="k">fi </span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"Starting WSL ssh server ... "</span> <span class="nb">sudo</span> /etc/init.d/ssh start 2&gt;&amp;1 1&gt;/dev/null <span class="o">[</span> <span class="nv">$?</span> <span class="o">==</span> 0 <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"OK"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"Error"</span> </code></pre></div></div> <p>This script does the following things primarily:</p> <ul> <li>Forwards traffic going to port 22 from your Windows system to port 22 on the WSL virtual machine</li> <li>Adds a Windows firewall rule to allow port 22 traffic</li> <li>Starts the ssh server</li> </ul> <p>We will be configuring Windows to execute this script on startup. The reason this is necessary is that WSL obtains a new IP address each time the system starts. This results in the need to reset the port forwarding and reapply with the latest WSL IP address.</p> <h1 id="step-4-configure-super-user-execution">Step 4: Configure Super User Execution</h1> <p>As indicated in step 5 of <a href="https://faun.pub/how-to-setup-ssh-connection-on-ubuntu-windows-subsystem-for-linux-2b36afb943dc">this guide</a>, we need to allow the ssh command to be started without prompting for a password. We do this by editing the /etc/sudoers file. This can be done with the following command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">$ </span><span class="nb">sudo </span>visudo </code></pre></div></div> <div class="theme-note"> Note: <p>The purpose of editing the /etc/sudoers file using the visudo command is to validate the syntax before saving. You could edit the file directly, but if you screw something up then you could lock yourself out of gaining root access.</p> <p>When I first used this command, it launched using the nano editor with which I’m not familiar. You can configure which editor is used by executing the following command:</p> <pre> $ sudo select-editor </pre> <p>Alternately, you can set your EDITOR environment variable to the desired editor and use the following command:</p> <pre> $ sudo -E visudo </pre> </div> <p>Add the following as the last line of the file:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>%sudo <span class="nv">ALL</span><span class="o">=</span>NOPASSWD: /etc/init.d/ssh </code></pre></div></div> <h1 id="step-5-configure-windows-task-scheduler">Step 5: Configure Windows Task Scheduler</h1> <p>Our final step will be to configure Windows Task Scheduler to launch our startup script when the system starts. Use the following steps:</p> <ul> <li> <p>Open the Task Scheduler app from the Windows Start Menu</p> </li> <li> <p>Select <code class="language-plaintext highlighter-rouge">Create Basic Task</code> from the right panel</p> </li> <li> <p>Create task with the following parameters:</p> <p>Name: Start SSH Server</p> <p>Description: Task to automate sshd startup</p> <p>Trigger: Select When the computer starts</p> <p>Action: Select Start a program</p> <p>Program/script :<code class="language-plaintext highlighter-rouge">%windir%\System32\wsl.exe</code> Add arguments (optional): <code class="language-plaintext highlighter-rouge">-d Ubuntu -e "/usr/local/bin/startup-ssh.sh"</code></p> </li> <li> <p>Confirm everything is correct and click <code class="language-plaintext highlighter-rouge">Finish</code></p> </li> </ul> <h1 id="step-7-verify-configuration">Step 7: Verify Configuration</h1> <p>Our last step is to verify we have everything configured correctly. In Task Scheduler, locate the “Start SSH Server” task and in the right panel click “Run”. If successful, you should be able to ssh from another machine to your Windows WSL virtual machine:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ssh dgreer@dgreer-pc </code></pre></div></div> Perhaps Too Much Validation http://aspiringcraftsman.com/2022/06/22/perhaps-too-much-validation.html Aspiring Craftsman urn:uuid:edcaf6d8-249b-709a-6250-5361404dc9fa Wed, 22 Jun 2022 08:00:00 +0000 Several factors have influenced my coding style over the years leaving me with a preference toward lean code syntax. I’ve been developing for quite a while, so it would be hard to pinpoint exactly when, where, or from whom I’ve picked up various preferences, but to name a few, I prefer: code that only includes comments for public APIs or to provide explanation of algorithms; code that is free of the use of regions, explicit default access modifiers, and unused using statements; reliance upon convention over configuration (both to eliminate repetitive tasks, but also just to eliminate unnecessary code); encapsulating excessive parameters into a Parameter Object, avoidance of excessive use of attributes/annotations (actually, I’d eliminate them completely if I could), and of course deleting dead code. There is one other practice I tend to see by other developers that I dislike and that’s too much validation. <p>Several factors have influenced my coding style over the years leaving me with a preference toward lean code syntax. I’ve been developing for quite a while, so it would be hard to pinpoint exactly when, where, or from whom I’ve picked up various preferences, but to name a few, I prefer: code that only includes comments for public APIs or to provide explanation of algorithms; code that is free of the use of regions, explicit default access modifiers, and unused using statements; reliance upon convention over configuration (both to eliminate repetitive tasks, but also just to eliminate unnecessary code); encapsulating excessive parameters into a <a href="https://wiki.c2.com/?ParameterObject">Parameter Object</a>, avoidance of excessive use of attributes/annotations (actually, I’d eliminate them completely if I could), and of course deleting dead code. There is one other practice I tend to see by other developers that I dislike and that’s too much validation.</p> <p>Perhaps you’ve seen code like this:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">MyService</span> <span class="p">{</span> <span class="k">public</span> <span class="nf">DoSomething</span><span class="p">(</span><span class="n">IDependencyA</span> <span class="n">dependencyA</span><span class="p">,</span> <span class="n">IDependencyB</span> <span class="n">dependencyB</span><span class="p">,</span> <span class="n">IDependencyC</span> <span class="n">dependencyC</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span><span class="p">(</span><span class="n">dependencyA</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">dependencyA</span><span class="p">));</span> <span class="p">}</span> <span class="k">if</span><span class="p">(</span><span class="n">dependencyB</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">dependencyB</span><span class="p">));</span> <span class="p">}</span> <span class="k">if</span><span class="p">(</span><span class="n">dependencyC</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">dependencyC</span><span class="p">));</span> <span class="p">}</span> <span class="p">}</span> <span class="err">…</span> <span class="p">}</span> </code></pre></div></div> <p>Perhaps you even think this is a best practice. Is it? As with many things, the answer is really: It depends. One of the things that has greatly shaped my views on several aspects of software development over the years is adopting Test-Driven Development. The “test” part of the name is really a hold-over from adapting the practice of writing Unit Tests for driving design. With Unit Testing, you’re <em>testing</em> the code you’ve written. With Test-Driven Development, you’re <em>constraining the design</em> of the code to meet a set of specifications. It’s really quite a difference and one you may not fully appreciate unless you fully buy in to doing it for an extended period of time.</p> <p>One of the side-effects of practicing TDD is that you don’t write code unless it’s needed to satisfy a failing test. The use of code coverage tools are basically superfluous for TDD practitioners, not to mention rendering far superior regression test suites. What, however, does this have to do with validation?</p> <p>When driving out implementation through a series of executable specifications (i.e. an objective list of exactly how the software should work), we may end up writing code which technically <em>could</em> be called a certain way which would result in exceptions or logical errors, but in <em>practice</em> never is. As it relates to this topic, all code we write can be grouped into two categories: public and private. In this sense I’m not talking about the access modifiers we place upon the code artifacts themselves, but the intended use of the code. Is the code you’re writing going to be used by others, or is it just code we’re calling internally within our applications? If it’s code you’re driving out through TDD which others will be calling, then you should have specifications which describe how the code will react when used correctly as well as incorrectly and thus will have the appropriate amount of validation. If it isn’t code anyone else will be, or currently is calling (see also YAGNI), then the components which <em>do</em> call it will have been designed such that they don’t call the component incorrectly rending such validation useless.</p> <p>Let’s consider our code again:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">MyService</span> <span class="p">{</span> <span class="k">public</span> <span class="nf">DoSomething</span><span class="p">(</span><span class="n">IDependencyA</span> <span class="n">dependencyA</span><span class="p">,</span> <span class="n">IDependencyB</span> <span class="n">dependencyB</span><span class="p">,</span> <span class="n">IDependencyC</span> <span class="n">dependencyC</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span><span class="p">(</span><span class="n">dependencyA</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">dependencyA</span><span class="p">));</span> <span class="p">}</span> <span class="k">if</span><span class="p">(</span><span class="n">dependencyB</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">dependencyB</span><span class="p">));</span> <span class="p">}</span> <span class="k">if</span><span class="p">(</span><span class="n">dependencyC</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentNullException</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">dependencyC</span><span class="p">));</span> <span class="p">}</span> <span class="p">}</span> <span class="err">…</span> <span class="p">}</span> </code></pre></div></div> <p>If this is an internal service that isn’t going to be called by any other code except other components within your application, we have 14 lines of code that are unneeded and are just adding noise to our code. I’ve worked in shops where every class in an application or library was coded this way, effectively adding hundreds to thousands of lines of unneeded code. Like regions, comments, or poorly factored code, this adds to the cognitive load required for reading through and understanding the code and ultimately is unnecessary. So the next time you reflexively start adding such validation, consider the possibility that perhaps you may be adding too much validation.</p> For Whom is this Container? http://aspiringcraftsman.com/2022/06/01/for-whom-is-this-container.html Aspiring Craftsman urn:uuid:a002a115-ef0f-7cf6-dc66-bfb35eedf380 Wed, 01 Jun 2022 08:00:00 +0000 Several of the Messaging platforms in the .Net space have pretty rudimentary APIs (e.g. RabbitMq, Kafka) which require quite a bit of boiler-plate code to be written to get a simple message published and subscribed. You could turn to one of the Conforming Abstraction libraries such as NServiceBus or MassTransit, but perhaps you don’t really want a lowest-common denominator API, you don’t like something about how it creates the messages or topic/queue artifacts, or you simply want a fluent API expressed in terms of the native platform’s nomenclature and behavior. This might lead you down the road of creating your own KafkaBus, SQSBus, RabbitMqBus, etc. that feels like the API you wish the original development team had just provided for you to begin with. Ah, but now you have a dilemma: Frameworks such as this tend to require a number of components you’ll need to compose, many of which you may want to allow users to configure (e.g. serialization needs, consumer class conventions, logging, produce and consume pipelines, etc.). You could write hand-rolled factories, builders, singletons, etc. to facilitate the configuration and building of instances of your components, but you know that using a dependency injection container would make both development and long-term maintenance of your library much easier. But now you have another dilemma: Are you going to tie your project to some open-source container? If so, which one? Should you support a handful of the most popular ones? Should you just rely upon the Service Locator pattern and provide configuration for end users should they want to resolve from their own containers? <p>Several of the Messaging platforms in the .Net space have pretty rudimentary APIs (e.g. RabbitMq, Kafka) which require quite a bit of boiler-plate code to be written to get a simple message published and subscribed. You could turn to one of the Conforming Abstraction libraries such as NServiceBus or MassTransit, but perhaps you don’t really want a lowest-common denominator API, you don’t like something about how it creates the messages or topic/queue artifacts, or you simply want a fluent API expressed in terms of the native platform’s nomenclature and behavior. This might lead you down the road of creating your own KafkaBus, SQSBus, RabbitMqBus, etc. that feels like the API you <em>wish</em> the original development team had just provided for you to begin with. Ah, but now you have a dilemma: Frameworks such as this tend to require a number of components you’ll need to compose, many of which you may want to allow users to configure (e.g. serialization needs, consumer class conventions, logging, produce and consume pipelines, etc.). You could write hand-rolled factories, builders, singletons, etc. to facilitate the configuration and building of instances of your components, but you know that using a dependency injection container would make both development and long-term maintenance of your library much easier. But now you have another dilemma: Are you going to tie your project to some open-source container? If so, which one? Should you support a handful of the most popular ones? Should you just rely upon the Service Locator pattern and provide configuration for end users should they want to resolve from their own containers?</p> <p>This was essentially the dilemma the ASP.Net Core team found themselves in when they set out to develop .Net Core. They had a fairly sizable framework with a lot of moving parts, many of which they wanted to allow the end user to configure. Earlier versions of ASP.Net MVC were built using a Service Locator pattern implementation which facilitated the ability to configure resolving from an open-source container of your choice. This, however, would have no doubt presented various design limitations in addition to the resulting lack of elegance to the resulting codebase, so the team decided to build the new platform from the ground up using dependency injection. They couldn’t, however, feasibly decide to couple their framework to one of the already mature and successful open source DI containers for various reasons. This prompted them to write their own.</p> <p>One of the keys to understanding the capabilities offered by .Net Core’s container compared to other libraries is recognizing that they built it for their needs, not yours. There is no doubt that there was recognition of the usefulness for some developers to have an out-of-the-box DI container, but they didn’t set out to build a container to compete with the already extremely mature frameworks such as Autofac, StructureMap, or Ninject. For instance, because they weren’t developing user interactive client-facing applications, they didn’t have needs such as convention-based scanning registration, multi-tenancy support, the need for decorators, etc. Their needs pretty much were limited to known types with lifetime scopes of transient, singleton, or scoped per request.</p> <p>Oddly, there is now a whole new generation of .Net developers which have never used a DI container other than that provided by the Microsoft Extensions suite which are missing out on being exposed to solutions to problems for which containers like Autofac, Lamar, and others facilitate fairly easily, largely I believe because no one has ever really told them: Microsoft didn’t <em>really</em> write that for you.</p> Pragmatic Deferral https://lostechies.com/derekgreer/2022/05/31/pragmatic-deferral/ Los Techies urn:uuid:b8e5fba6-f2d8-e502-10ab-7971b60ca56e Tue, 31 May 2022 13:00:00 +0000 Software engineering is often about selecting the right trade offs. While deferring feature development is often somewhat straight-forward, based upon a speculation about the return on investment, and generally decided by the customer; marketing; sales; or product people; low-level implementation decisions are typically made by the development team or individual developers and can often prove to be a bit more contentious among teams with a plurality of strong opinions. This is where principles like YAGNI (You’re Aren’t Going to Need It), or the Rule of Three have often been set forth as a guiding heuristic. <p>Software engineering is often about selecting the right trade offs. While deferring feature development is often somewhat straight-forward, based upon a speculation about the return on investment, and generally decided by the customer; marketing; sales; or product people; low-level implementation decisions are typically made by the development team or individual developers and can often prove to be a bit more contentious among teams with a plurality of strong opinions. This is where principles like YAGNI (You’re Aren’t Going to Need It), or the Rule of Three have often been set forth as a guiding heuristic.</p> <p>While I generally advise the teams I coach to allow the executable specifications (i.e. the tests) to drive emergent design and to defer the introduction of ancillary libraries, frameworks, patterns, and custom infrastructure, until you need it, there is a level of pragmatism that I employee when determining when to introduce such things.</p> <p>I’ve been a fan of Test-Driven Development for some time now and have practiced it for over a decade. One of the primary benefits of Test-Driven Development is having an objective measure guiding what needs to get built. For example, if the acceptance criteria for a User Story concerns building a new Web API for a company’s custom B2B solution, your specs are going to drive out some sort of HTTP-based API. What the specs won’t dictate, however, are decisions such as whether to use an MVC framework, an IOC container, whether to introduce a fluent validation library or an object mapping library. Should we adhere strictly to principles like <a href="https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it">YAGNI</a> or the <a href="https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)">Rule of Three</a> for guidance here? My answer is: it depends.</p> <p>Deferring software decisions comes with quite a range of consequences. Some decisions, such as whether to select ASP.NET MVC at the outset of a .Net-based Web application, could cause quite a bit of rework if you were to defer such a decision until working with lower-level components started to reveal friction or duplication. Other decisions, such as deferring the introduction of an object mapping library (e.g. Automapper) until the shape of the objects you’re returning actually differ from your entities essentially have only positive consequences. But how do we know?</p> <p>The YAGNI principle is very similar to the firearm safety rule “The Gun is Always Loaded”. No, the gun isn’t always loaded … but it’s best to treat it like it is. Similarly, “You aren’t going to need it” doesn’t really mean you may not need it, but it’s intended to help you avoid unnecessary work. That is, until it causes more work.</p> <p>In software engineering, the more you code, the more you’ll have to maintain. The Art of Not Doing Stuff, when correctly applied, can save companies as much or more money than building the right things. While I’m not religious these days, there’s a definition of the term “Hermeneutics” that I heard years ago from a Christian radio personality, Hank Hanegraaff. He would say: “Hermeneutics is the art and science of biblical interpretation”. He would go on to explain, it’s a science because it’s guided by a system of rules, but it’s an art in that you get better at it the more you do it. Having heard that explanation years ago, I have long felt these properties are equally descriptive of software development.</p> <p>For myself, I take a pragmatic approach to YAGNI in that I make selections for a number of things at the outset of a new project which I’ve recognized, through experience, has resulted in less friction down the road; and I defer choices which I reason to have little to no cost by implementing at the point implementing a given User Story’s acceptance criteria drives the need. For example, I do start off setting up a Web project using ASP.NET MVC. I do set up end-to-end testing infrastructure. I do add an open source DI container and set up convention-based registration. These are things which I’ve found actually cause me more friction if I pretend I’m not going to need them. I don’t want to implement my own IHttpHandler and wait until I see the need for a robust routing and pipeline framework and have to go back and reimplement everything. I don’t want to be hand-rolling factories over and over and have to go back and modify code at the point enough duplication reveals the need for dependency injection, and I don’t want to edit a Startup.cs or other bootstrapper component each time a component has a new dependency. Outside of these few concerns, however, I do typically defer things until needed.</p> Magical Joy https://lostechies.com/derekgreer/2022/05/27/magical-joy/ Los Techies urn:uuid:51894de0-4304-2d28-6aa8-57666890c876 Fri, 27 May 2022 13:00:00 +0000 In a segment of an interview with host Byron Sommardahl on The Driven Developer Podcast, recorded in the summer of 2021, Byron and I discussed a bit about a pattern I introduced to our project when we worked together in 2010 which Byron later dubbed “The Magical Joy Bus” <p>In a segment of an interview with host Byron Sommardahl on <a href="https://podcasts.apple.com/us/podcast/all-things-senior-derek-greer/id1584867029?i=1000541910261">The Driven Developer Podcast</a>, recorded in the summer of 2021, Byron and I discussed a bit about a pattern I introduced to our project when we worked together in 2010 which Byron later dubbed “The Magical Joy Bus” Nine Years Remote http://aspiringcraftsman.com/2022/05/26/nine-years-remote.html Aspiring Craftsman urn:uuid:3032cd6e-d96e-34a3-3acc-4ff8654e9927 Thu, 26 May 2022 13:00:00 +0000 A recent inquiry from a recruiter about accepting a partially-remote position prompted me to reflect upon 9 years of working remotely as a software developer. <p>A recent inquiry from a recruiter about accepting a partially-remote position prompted me to reflect upon 9 years of working remotely as a software developer.</p> <p>When I first started working from home, attitudes were quite different than they are in today’s post COVID-19 world. Full time remote software development jobs were few and far between, and most employers that allowed working remotely full time did so due to factors other than a belief that it was more productive and cost-effective. Studies since have overwhelmingly shown that the majority were simply wrong.</p> <p>One interesting side-effect of the previous year’s COVID-19 political entanglement is the degree to which it forced an entire generation of closed-minded, micro-managing executives to consider (through necessity) that remote work forces, especially for primarily thoughtwork-based positions, were not only viable, but perhaps even superior.</p> <p>When our entire society started shutting down due to concerns over the COVID-19 virus, I actually hardly noticed at first. Having transitioned to full-time remote work in early 2014, I had long since become accustomed to working remotely by the time society started shutting down. Prior to landing my first full-time remote position, I had worked at a couple of prior companies which allowed working remotely a couple of days a week, so I had some notion of its viability even before then.</p> <p>While I was already used to working remotely, the whole pandemic thing actually helped to improve the lives of remote developers by remedying many of the productivity nuciences that plagued fully-remote as well as mixed-teams. To a large extent, the primary issues that remote workers had to face prior to everything being shut down was the lack of remote workforce accommodations, namely: mature or provided collaboration tools (e.g Slack, Zoom, Miro, etc.) and equal participation of remote workers on mixed-teams. While David Fullerton, in a <a href="https://stackoverflow.blog/2013/02/01/why-we-still-believe-in-working-remotely/">StackOverflow blog article</a> written back in 2013, had proffered up the wisdom that “<em>If even one person on the team is remote, every single person has to start communicating online</em>”, joining any mixed team for many still resulted in the remote worker likely being marginalized in meetings as they were the only one on a call while all their co-workers debated approaches around a conference table, were forced to watch some white boarding design session over a video camera while you tried to make out what everyone was saying, or were simply being left out of key social interactions resulting in being professionally disadvantaged in key business decisions due to the formation of clicks, or simply not being present during unplanned discussions, etc. Conscientious employees working from home already knew they were far more efficient at home than in the office, as well as knowing that non-conscientious workers were just a likely or more so to screw off at work as they were at home, but it took everyone being forced to do it for an extended period of time to hammer than into the heads of many executives that felt uncomfortable with conducting business differently than they had in the 20th century.</p> <p>One absolutely huge thing that goes seemingly undiscussed is the financial impact of working remotely vs. commuting. While I commuted to the office for 20 years before transitioning to full-time remote, it wasn’t until I had become accustomed to working from home and was confronted with the idea of returning back to the world of the commuting zombies that my perspective changed with respect to that commute time. Prior to accepting a full time remote job in early 2014, my commute time was approximately 1 hour one way, and that was on a good day when there wasn’t some minor traffic incident which could easily (and fairly regularly did) add an extra 20-30 minutes to my time. Once I had become accustomed to working remotely, the idea of tacking on an extra 5-10 hours a week in commute time to switch back to a job requiring you to work in the office seemed more like giving my time away for free. Prior to that, all those hours in the vehicle dealing with idiots on the road was just an assumed necessity. Driving to work was like driving anywhere else. Of course in the 20th century you had to drive to buy a new pair of shoes. Of course you had to drive to see a newly released movie. Of course you had to drive to go get a cheeseburger meal at McDonald’s. And of course, you had to drive to get to work. You didn’t think twice about it. You didn’t view commuting to work as 5-10 hours of your personal time given over to your employer for free for the privilege of employment any more than you’d have thought that McDonald’s owed you money for driving to their store to eat. Sure, you could listen to music, or talk radio, or a podcast, or an audio book. It wasn’t, however, really what you would have chosen to be doing at 6:30 in the morning. It wasn’t <em>your</em> time.</p> <p>Prior to COVID, trying to explain this perspective to those still in the office world was very much like Morpheus trying to explain to Neo that he’s in the Matrix. Sure, recruiters or employers could understand the logic of an argument that commuting is time given to an employer essentially for free, but many would just think it ridiculous for you to go so far as to demand a higher salary for accepting a position requiring a commute (when you knew is wasn’t really required to do the job). This doesn’t even account for wear and tear on vehicles, gas expenses, or the little micro-batches of time you end up spending doing things like food prep, additional “get ready” time, more laundry, etc. that you wouldn’t otherwise do if you were staying home for the day. Moreover, just because you compensate someone for their time, there’s a threshold beyond which your standard hourly rate isn’t worth the time. Okay, you may be willing to commute if your employer is going to compensate you for the extra 5-10 hours on top of the 40 you’re going to spend sitting in their cube farm under their fluorescent lighting (“Not near a window, Jim, because those seats are reserved for managers!”). Are you, however, willing to exchange that extra 5-10 hours a week for money to sit in the office for 45 hours? How about 50 hours? 60? At some point, it isn’t about whether you’re compensated or not. Hell, 40 hours a week really is too damn many hours to begin with. Add to that the insane perspective on time off that Americans get on average compared to much of the rest of the developed world. Hell, even plumbers and HVAC workers get paid for their commute time, and their job isn’t something that can be done remotely.</p> <p>Imagine if everyone actually accounted for these additional expenses when factoring in the pay they are willing to accept. If so, this would likely account for an extra 25-30% pay increase, accounting for time and travel expenses. For businesses on the fence about whether remote is better than on-site for their bottom line, this would certainly tip the scales. Currently, however, they aren’t forced to think this way. Or at least, many are still operating in a mindset that they don’t have to think this way. When it really comes down to it, a culture of requiring anyone that can do their job remotely to work in the office is really stealing from your employees. Fortunately, COVID has corrected this situation given there have been enough eyes opened to the benefits of remote work and enough businesses which have seen the waste that goes into buying or renting commercial real estate that, even after many business have begun attempting to force employees back into the office, there’s enough employers who now offer remote opportunities to give people a real choice.</p> User Stories http://aspiringcraftsman.com/2022/05/25/user-stories.html Aspiring Craftsman urn:uuid:6950fc71-b80c-4387-4204-d3034b883c82 Wed, 25 May 2022 13:00:00 +0000 The use of User Stories has become fairly commonplace in the software industry. First introduced as an agile requirements-gathering process by Extreme Programming, User Stories arguably owe their popularity most to the adoption of the Scrum framework for which User Stories have become the de facto expression of its prescribed backlog. <p>The use of User Stories has become fairly commonplace in the software industry. First introduced as an agile requirements-gathering process by Extreme Programming, User Stories arguably owe their popularity most to the adoption of the Scrum framework for which User Stories have become the de facto expression of its prescribed backlog.</p> <p>So what exactly is a User Story? Put simply, they are a light-weight approach to expressing the desired needs of a software system. The idea behind User Stories, which was introduced as simply “Stories” in the book <em>Extreme Programming Explained - Embrace Change</em> by Kent Beck, was to move away from rigid requirements gathering processes in process, form, and nomenclature. Beck explained that the very word “requirement” was an inhibitor to embracing change because of its connotations of absolutism and permanence. At their inception, the intended form of stories was to create an index card containing a short title, simple description written in prose, and an estimation.</p> <h2 id="the-three-part-template">The Three-Part Template</h2> <p>In the late 1990’s, a software company named Connextra was an early adopter of Extreme Programming. In contrast to the distinct roles defined by the Scrum framework, XP doesn’t prescribe any specific roles, but is intended to adapt to existing roles within an organization (e.g. project managers, product managers, executives, technical writers, developers, testers, designers, architects, etc.).</p> <p>The origin of most of Connextra’s stories were from members of their Marketing and Sales departments which wrote down a simple description of features they desired. This posed a problem for the development team, however, for when the time came to have a conversation about the feature, the development team often had difficulty locating the original stakeholder to begin the conversation. This led the team to formulate a 3-part template to help address friction resulting from ambiguous requirement sources. Their 3-part template is as follows:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> As a [type of user] I want to [do something] So that I can [get some benefit] </code></pre></div></div> <p>Ironically, while the 3-part template has become the defacto standard for authoring User Story descriptions, Scrum’s “Product Owner” role, most often filled by product development specialists acting as customer proxies, along with the use of software agile-planning tools such as Confluence, Planview, Azure DevOps Boards, etc., which captures who created a given story, tends to greatly diminish the need from which the template originated. This template has since become quite the de facto standard in expressing User Story Descriptions. The irony is that many teams, in caro-cult fashion, often utilize the 3-part template where the original need to identify the author of the story to start the conversation no longer exists. Change has occurred, but because many didn’t understand the underlying impetus for the 3-part template, they were incapable of <em>adapting</em> to that change.</p> <p>Jeff Patton writes the following concerning the prevalent use of the 3-part story template in his book “User Story Mapping”:</p> <blockquote> <p>“… the template has become so ubiquitous, and so commonly taught, that there are those who believe that it’s not a story if it’s not written in that form. … All of this makes me sad. Because the real value of stories isn’t what’s written down on the card. It comes from what we learn when we tell the story.”</p> </blockquote> <p>Mike Cohn, author of many books on agile processes including “User Stories Applied” and “Agile Estimating and Planning” writes similarly:</p> <blockquote> <p>“Too often team members fall into a habit of beginning each user story with “As a user…” Sometimes this is the result of lazy thinking and the story writers need to better understand the product’s users before writing so many “as a user…” stories.”</p> </blockquote> <p>Cohn’s observations are spot on. In my experience, not only does this happen “too often”, it’s the rule, not the exception. It’s really just human nature. The moment a process becomes formulaic, teams will begin to just go through the motions without engaging their minds. This can be good for manual tasks like brick-laying, or cleaning a house, but it is detrimental to processes intended to promote communication. Sadly, many teams spend an inordinate amount of time on the trappings of things like ensuring their requirements follow the 3-part story template rather than using the story as a tool for its original intent: A placeholder for a conversation.</p> <h2 id="there-and-back-again">There and Back Again</h2> <p>While not explicitly stated, the original idea behind Stories in Extreme Programming was to facilitate a conversation, not to define an objective goal. The agile movement started as a way to address issues in the industry’s largely failing attempts to apply manufacturing processes to software development. In particular, Stories were intended to address the underlying motivation for requirements (i.e. how teams determine what to build), not to themselves be requirements.</p> <p>In many ways, today’s User Stories have become the antithesis of what Kent Beck originally intended. Sadly, much of what is marketed as “agile” today has been corrupted by traditional-minded business analysts, product managers, and marketing agencies who never really understood the agile movement fully. User Stories have, to a large extent, become a casualty of these groups. We’ve gone from requirements to stories and back again. As described by Jeff Patton, <em>“Stories aren’t a way to write better requirements, but a way to organize and have better conversations.</em>”</p> <h2 id="the-better-way">The Better Way</h2> <p>Ultimately, the question companies seek to answer is: How do we determine the features which provide the best ROI for the business? While it may seem counterintuitive to some, customers aren’t generally the best source for determining what features to build. They can be <em>a</em> source, but they aren’t generally a team’s best source. Customers are, however, the best source for determining how customers currently work, what problems they face, and what friction is involved in any current processes. Various analysis techniques can be used to solicit customer opinions on desired features, but it’s best to rely upon such techniques merely as means to distill the problems currently faced by customers. From there, stories are best created with a simple title and a description of the customer’s problem written in prose with the intent for the description to serve as a starting point for a conversation with the team.</p> <p>The best way to determine what to build is as a member of a mature agile team. The operative word here is <em>mature</em>. What makes for a mature team is a Product Owner with a background in the problem domain space, a Team Coach with deep knowledge of agile and lean processes, and 3-5 cross-functional developers weighted toward senior experience who have gone through a forming, storming, norming, and performing phase.</p> <p>User Stories shouldn’t be feature requests, but rather a placeholder for a conversation. A conversation with whom? With your team. About what? About how to iteratively solve the problems you learned from customers in small steps with frequent feedback. Product Owners should not bring requirements to a development team. There’s great power in collaboration. A smart team of 5 to 7 individuals including a subject matter expert (what the Product Owner should bring to the table) and a coach are a far better source for what features to build than just the customer or the Product Owner.</p> <h2 id="an-example">An Example</h2> <p>The following is an example story which more closely follows the original intent of Stories.</p> <p>Our scenario involves a company which provides a website allowing customers to create wedding and gift registries to send to others. In its current form, the site allows customers to pick from among existing vendors, but the company frequently receives requests from customers about specific products they’d like to see included. The current process involves the Sales team creating tickets for their Operations team to add new vendors to the site which involves updating the production database directly. Additionally, the work currently falls to one person whose job entails other operation tasks which often results in a delay to the timely fulfillment of customer requests.</p> <p>The following represents the story:</p> <table style="border: 1px solid black; background-color: white; color: black"> <tr> <td> <h2>Easily Manage Registry Products</h2> <hr style="border-top: 1px solid black" /> <h3>Description</h3> Our customers often want to add products that aren't part of our current vendor product list. This causes the sales team to constantly have to put in tickets and currently Margret is the only one that is working the tickets. We need a better solution! </td> </tr> </table> <p>Note how the description is written in prose (i.e. in normal conversational language), and doesn’t follow the wooden 3-part template. Note also, the story doesn’t prescribe <em>how</em> to solve the problem. It just provides background on what the problem is and who it affects. It isn’t <em>just</em> that the story doesn’t dictate implementation details, but that it doesn’t dictate the solution <em>at all</em>. This is the ideal starting point for most stories. It’s a placeholder for a conversation about how to solve the problem.</p> <p>From here, the team would collaborate on the story to determine the best solution that results in the smallest feature increment which adds value to the end user. Several ideas may be discussed. The system could integrate with a 3rd-party content management system, allowing people within the company without SQL experience to update content. Alternately, the team may decide that adding a feature to allow customers to add custom products directly to their personal event registry is both easier, and scales far better than solutions requiring company employees to work tickets.</p> <p>As part of a story refinement session, the team may update the story with acceptance criteria to guide the implementation:</p> <table style="border: 1px solid black; background-color: white; color: black"> <tr> <td> <h2>Easily Manage Registry Products</h2> <hr style="border-top: 1px solid black" /> <h3>Description</h3> Our customers often want to add products that aren't part of our current vendor product list. This causes the sales team to constantly have to put in tickets and currently Margret is the only one that is working the tickets. We need a better solution! <br /><br /> <h3>Acceptance Criteria</h3> <b>When the customer navigates to the edit registry view</b><br /> &nbsp;&nbsp;it should contain a link for adding custom products <br /><br /> <b>When the customer clicks the add custom product link</b><br /> &nbsp;&nbsp;it should navigate to the add custom product view (note: see balsamiq wireframe attached) <br /><br /> <b>When the customer adds a new custom product with valid inputs</b><br /> &nbsp;&nbsp;it should add the custom product to the customers registry<br /> &nbsp;&nbsp;it should display a success message in the application banner<br /> &nbsp;&nbsp;it should navigate back to the edit registry page <br /><br /> <b>When the customer enters invalid custom product parameters</b><br /> &nbsp;&nbsp;it should show standard field level error messages<br /> &nbsp;&nbsp;it should not enable the save button<br /> </td> </tr> </table> <p>While an Acceptance Criteria section isn’t mandatory, it can often be valuable for helping to frame the scope of the story, a reminder to the team of the high-level plans discussed for deferred work, and/or may serve as the team’s Definition of Done. For small teams involving just a few members, or for highly adaptive and collaborative teams, it may be enough to just just write “<em>We decided to add a feature to allow the customer to add their own products!</em>”. The team may very well take the initial story description and rapidly iterate on a solution, deciding together when they think it’s done! (Gasp!) Of course, this level of informality probably is only best suited to highly cohesive, highly functioning teams. For inexperienced to moderately experienced teams, some denotation of Acceptance Criteria would be advisable. The key point is, the story didn’t arrive to the team in the form of requirements, but as a placeholder for a conversation.</p> <h2 id="conclusion">Conclusion</h2> <p>As the adoption of agile frameworks such as Scrum have become more mainstream, a number of practices have become formulaic and adopted by teams via a cargo-cult onboarding to agile practices without truly grasping what it means to be agile. The User Story has all but lost it original intent by many teams who have done little more than slap agile labels onto Waterfall manufacturing processes. User Stories were never intended to be requirements, but rather a placeholder for a conversation with the development team. Let’s do better.</p> .Net Project Builds with Node Package Manager http://aspiringcraftsman.com/2020/12/10/dotnet-project-builds-with-npm.html Aspiring Craftsman urn:uuid:f5b63a4e-aa03-614f-1f0c-291269ba15b5 Thu, 10 Dec 2020 07:00:00 +0000 A few years ago, I wrote an article entitled Separation of Concerns: Application Builds &amp; Continuous Integration wherein I discussed the benefits of separating project builds from CI/CD concerns by creating a local build script which lives with your project. Not long after writing that article, I was turned on to what I’ve come to believe is one of the easiest tools I’ve encountered for managing .Net project builds thus far: npm. <p>A few years ago, I wrote an article entitled <a href="http://aspiringcraftsman.com/2016/02/28/separation-of-concerns-application-builds-continuous-integration/">Separation of Concerns: Application Builds &amp; Continuous Integration</a> wherein I discussed the benefits of separating project builds from CI/CD concerns by creating a local build script which lives with your project. Not long after writing that article, I was turned on to what I’ve come to believe is one of the easiest tools I’ve encountered for managing .Net project builds thus far: npm.</p> <p>Most development platforms provide a native task-based build technology. Microsoft’s tooling for these needs is MSBuild: a command-line tool whose build files double as Visual Studio’s project and solution definition files. I used MSBuild briefly for scripting custom build concerns for a couple of years, but found it to be awkward and cumbersome. Around 2007, I abandoned use of MSBuild for creating builds and began using Rake. While it had the downside of requiring a bit of knowledge of Ruby, it was a popular choice among those willing to look outside of the Microsoft camp for tooling and had community support for working with .Net builds through the <a href="https://www.codemag.com/article/1006101/Building-.NET-Systems-with-Ruby-Rake-and-Albacore">Albacore</a> library. I’ve used a few different technologies since, but about 5 years ago I saw a demonstration of the use of npm for building .Net projects at a conference and I was immediately sold. When used well, it really is the easiest and most terse way to script a custom build for the .Net platform I’ve encountered.</p> <p>“So what’s special about npm?” you might ask. The primary appeal of using npm for building applications is that it’s easy to use. Essentially, it’s just an orchestration of shell commands.</p> <h3 id="tasks">Tasks</h3> <p>With other build tools, you’re often required to know a specific language in addition to learning special constructs peculiar to the build tool to create build tasks. In contrast, npm’s expected package.json file simply defines an array of shell command scripts:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Clean the project."</span><span class="p">,</span><span class="w"> </span><span class="nl">"restore"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Restore dependencies."</span><span class="p">,</span><span class="w"> </span><span class="nl">"compile"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Compile the project."</span><span class="p">,</span><span class="w"> </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Run the tests."</span><span class="p">,</span><span class="w"> </span><span class="nl">"dist"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Create a distribution."</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Some author"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>As with other build tools, NPM provides the ability to define dependencies between build tasks. This is done using pre- and post- lifecycle scripts. Simply, any task issued by NPM will first execute a script by the same name with a prefix of “pre” when present and will subsequently execute a script by the same name with a prefix of “post” when present. For example:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Clean the project."</span><span class="p">,</span><span class="w"> </span><span class="nl">"prerestore"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm run clean"</span><span class="p">,</span><span class="w"> </span><span class="nl">"restore"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Restore dependencies."</span><span class="p">,</span><span class="w"> </span><span class="nl">"precompile"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm run restore"</span><span class="p">,</span><span class="w"> </span><span class="nl">"compile"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Compile the project."</span><span class="p">,</span><span class="w"> </span><span class="nl">"pretest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm run compile"</span><span class="p">,</span><span class="w"> </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Run the tests."</span><span class="p">,</span><span class="w"> </span><span class="nl">"prebuild"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm run test"</span><span class="p">,</span><span class="w"> </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Publish a distribution."</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Some author"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Based on the above package.json file, issuing “npm run build” will result in running the tasks of clean, restore, compile, test, and build in that order by virtue of each declaring an appropriate dependency.</p> <p>Given you’re okay with limiting a fully-specified dependency chain where a subset of the build can be initiated at any stage (e.g. running “npm run test” and triggering clean, restore, and compile first) , the above orchestration can be simplified by installing the npm-run-all node dependency and defining a single pre- lifetime script for the main build target:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Clean the project."</span><span class="p">,</span><span class="w"> </span><span class="nl">"restore"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Restore dependencies."</span><span class="p">,</span><span class="w"> </span><span class="nl">"compile"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Compile the project."</span><span class="p">,</span><span class="w"> </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Run the tests."</span><span class="p">,</span><span class="w"> </span><span class="nl">"prebuild"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm-run-all clean restore compile test"</span><span class="p">,</span><span class="w"> </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"echo Publish a distribution."</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"npm-run-all"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.1.5"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>In this example, issuing “npm run build” will result in the prebuild script executing npm-run-all with the parameters: clean, restore, compile and test which it will execute in the order listed.</p> <h3 id="variables">Variables</h3> <p>Aside from understanding how to utilize the pre- and post- lifecycle scripts to denote task dependencies, the only other thing you really need to know is how to work with variables.</p> <p>Node’s npm command facilitates the definition of variables by command-line parameters as well as declaring package variables. When npm executes, each of the properties declared within the package.json are flattened and prefixed with “npm_package_”. For example, the standard “version” property can be used as part of a dotnet build to denote a project version by referencing ${npm_package_version}:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"configuration"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Release"</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotnet build ./src/*.sln /p:Version=${npm_package_version}"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"npm-run-all"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.1.5"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Command-line parameters can also be passed to npm and are similarly prefixed with “npm_config_” with any dashes (“-”) replaced with underscores (“_”). For example, the previous version setting could be passed to dotnet.exe in the following version of package.json by issuing the below command:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build --product-version=2.0.0 </code></pre></div></div> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"configuration"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Release"</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotnet build ./src/*.sln /p:Version=${npm_config_product_version}"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"npm-run-all"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.1.5"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>(Note: the parameter –version is an npm parameter for printing the version of npm being executed and therefore can’t be used as a script parameter.)</p> <p>The only other important thing to understand about the use of variables with npm is that the method of dereferencing is dependent upon the shell used. When using npm on Windows, the default shell is cmd.exe. If using the default shell on Windows, the version parameter would need to be deference as %npm_config_product_version%:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"configuration"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Release"</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotnet build ./src/*.sln /p:Version=%npm_config_product_version%"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"npm-run-all"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.1.5"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <p>Until recently, I used a node package named “cross-env” which allows you to normalize how you dereference variables regardless of platform, but for several reasons including cross-env being placed in maintenance mode, the added dependency overhead, syntax noise, and support for advanced variable expansion cases such as default values, I’d recommend any cross-platform execution be supported by just standardizing on a single shell (e.g. “Bash”). With the introduction of Windows Subsystem for Linux and the virtual ubiquity of git for version control, most developer Windows systems already contain the bash shell. To configure npm to use bash at the project level, just create a file named .npmrc at the package root containing the following line:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>script-shell=bash </code></pre></div></div> <h3 id="using-node-packages">Using Node Packages</h3> <p>While not necessary, there are many CLI node packages that can be easily leveraged for aiding in authoring your builds. For example, a package named “rimraf”, which functions like Linux’s “rm -rf” command, is a utility you can use to implement a clean script for recursively deleting any temporary build folders created as part of previous builds. In the following package.json build, a package target builds a NuGet package which it outputs to a dist folder in the package root. The rimraf command is used to delete this temp folder as part of the build script’s dependencies:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rimraf dist"</span><span class="p">,</span><span class="w"> </span><span class="nl">"prebuild"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npm run clean"</span><span class="p">,</span><span class="w"> </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dotnet pack ./src/ExampleLibrary/ExampleLibrary.csproj -o dist /p:Version=${npm_package_version}"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w"> </span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ISC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"npm-run-all"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.1.5"</span><span class="p">,</span><span class="w"> </span><span class="nl">"rimraf"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.0.2"</span><span class="w"> </span><s Conventional Options http://aspiringcraftsman.com/2020/11/20/conventional-options.html Aspiring Craftsman urn:uuid:e938f7d3-0764-618b-470f-33236500c31d Fri, 20 Nov 2020 07:00:00 +0000 I’ve really enjoyed working with the Microsoft Configuration libraries introduced with .Net Core approximately 5 years ago. The older XML-based API was quite a pain to work with, so the ConfigurationBuilder and associated types provided a long overdue need for the platform. <p>I’ve really enjoyed working with the Microsoft Configuration libraries introduced with .Net Core approximately 5 years ago. The older XML-based API was quite a pain to work with, so the ConfigurationBuilder and associated types provided a long overdue need for the platform.</p> <p>I had long since adopted a practice of creating discrete configuration classes populated and registered with a DI container over direct use of the ConfigurationManager class within components, so I was pleased to see the platform nudge developers in this direction through the introduction of the IOptions<T> type.</T></p> <p>A few aspects surrounded the prescribed use of the IOptions<T> type of which I wasn't particularly fond were needing to inject IOptions<T> rather than the actual options type, taking a dependency upon the Microsoft.Extensions.Options package from my library packages, and the cermony of binding the options to the IConfiguration instance. To address these concerns, I wrote some extension methods which took care of binding the type to my configuration by convention (i.e. binding a type with a suffix of Options to a section corresponding to the option type's prefix) and registering it with the container.</T></T></p> <p>I’ve recently released a new version of these extensions supporting several of the most popular containers as an open source library. You can find the project <a href="http://github.com/derekgreer/conventional-options">here</a>.</p> <p>The following are the steps for using these extensions:</p> <h3 id="step-1">Step 1</h3> <p>Install ConventionalOptions for the target DI container:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$&gt; nuget install ConventionalOptions.DependencyInjection </code></pre></div></div> <h3 id="step-2">Step 2</h3> <p>Add Microsoft’s Options feature and register option types:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">services</span><span class="p">.</span><span class="nf">AddOptions</span><span class="p">();</span> <span class="n">services</span><span class="p">.</span><span class="nf">RegisterOptionsFromAssemblies</span><span class="p">(</span><span class="n">Configuration</span><span class="p">,</span> <span class="n">Assembly</span><span class="p">.</span><span class="nf">GetExecutingAssembly</span><span class="p">());</span> </code></pre></div></div> <h3 id="step-3">Step 3</h3> <p>Create an Options class with the desired properties:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">public</span> <span class="k">class</span> <span class="nc">OrderServiceOptions</span> <span class="p">{</span> <span class="k">public</span> <span class="kt">string</span> <span class="n">StringProperty</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="k">public</span> <span class="kt">int</span> <span class="n">IntProperty</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <h3 id="step-4">Step 4</h3> <p>Provide a corresponding configuration section matching the prefix of the Options class (e.g. in appsettings.json):</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"OrderService"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"StringProperty"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Some value"</span><span class="p">,</span><span class="w"> </span><span class="nl">"IntProperty"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div></div> <h3 id="step-5">Step 5</h3> <p>Inject the options into types resolved from the container:</p> <div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span> <span class="p">{</span> <span class="k">public</span> <span class="nf">OrderService</span><span class="p">(</span><span class="n">OrderServiceOptions</span> <span class="n">options</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ... use options</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Currently ConventionalOptions works with Microsoft’s DI Container, Autofac, Lamar, Ninject, and StructureMap.</p> <p>Enjoy!</p> Collaboration vs. Critique http://aspiringcraftsman.com/2018/05/18/collaboration-vs-critique.html Aspiring Craftsman urn:uuid:d7ab1cc0-12cd-bac9-a131-795e6dd47f3d Fri, 18 May 2018 17:00:00 +0000 While there are certainly a number of apps developed by lone developers, it’s probably safe to say that the majority of professional software development occurs by teams. The people aspect of software development, more often than not, tends to be the most difficult part of software engineering. Unfortunately the software field isn’t quite like other engineering fields with well-established standards, guidelines, and apprenticeship programs. The nature of software development tends to follow an empirical process model rather than a defined process model. That is to say, software developers tend to be confronted with new problems every day and most of the problems developers are solving aren’t something they’ve ever done in the exact same way with the exact same toolset. Moreover, there are often many different ways to solve the same problem, both with respect to the overall process as well as the implementation. This means that team members are often required to work together to determine how to proceed. Teams are often confronted with the need to explore multiple competing approaches as well as review one another’s designs and implementation. One thing I’ve learned during the course of my career is that the stage these types of interactions occur within the overall process has a significant impact on whether the interaction is generally viewed as collaboration or critique. <p>While there are certainly a number of apps developed by lone developers, it’s probably safe to say that the majority of professional software development occurs by teams. The people aspect of software development, more often than not, tends to be the most difficult part of software engineering. Unfortunately the software field isn’t quite like other engineering fields with well-established standards, guidelines, and apprenticeship programs. The nature of software development tends to follow an empirical process model rather than a defined process model. That is to say, software developers tend to be confronted with new problems every day and most of the problems developers are solving aren’t something they’ve ever done in the exact same way with the exact same toolset. Moreover, there are often many different ways to solve the same problem, both with respect to the overall process as well as the implementation. This means that team members are often required to work together to determine how to proceed. Teams are often confronted with the need to explore multiple competing approaches as well as review one another’s designs and implementation. One thing I’ve learned during the course of my career is that the stage these types of interactions occur within the overall process has a significant impact on whether the interaction is generally viewed as collaboration or critique.</p> <p>To help illustrate what I’ve seen happen countless times both in catch-up design sessions and code reviews, consider the following two scenarios:</p> <h3 id="scenario-1">Scenario 1</h3> <p>Tom and Sally are both developers on a team maintaining a large-scale application. Tom takes the next task in the development queue which happens to have some complex processes that will need to be addressed. Being the good development team that they are, both Tom and Sally are aware of the requirements of the application (i.e. how the app needs to work from the user’s perspective), but they have deferred design-level discussions until the time of implementation. After Tom gets into the process a little, seeing that the problem is non-trivial, he pings Sally to help him brainstorm different approaches to solving the problem. Tom and Sally have been working together for over a year and have become accustomed to these sort of ad-hoc design sessions. As they begin discussing the problem, they each start tossing ideas out on the proverbial table resulting in multiple approaches to compare and contrast. The nature of the discussion is such that neither Tom nor Sally are embarrassed or offended when the other points out flaws in a given design idea because there’s a sense of safety in their mutual understanding that this is a brainstorming session and that neither have thought in depth about the solutions being set forth yet. Tom throws out a couple of ideas, but ends up shooting them down himself as he uses Sally as a sounding board for the ideas. Sally does the same, but toward the end of the conversation suggests a slight alteration to one of Tom’s initial suggestions that they think may make it work after all. They end the session with a sense that they’ve worked together to arrive at the best solution.</p> <h3 id="scenario-2">Scenario 2</h3> <p>Bill and Jake are developers on another team. They tend to work in a more siloed fashion, but they do rely upon one another for help from time to time and they are required to do code reviews prior to their code being merged into the main branch of development. Bill takes the next task in the development queue and spends the better part of an afternoon working out a solution with a basic working skeleton of the direction he’s going. The next day he decides that it might be good to have Jake take a look at the design to make him aware of the direction. Seeing where Bill’s design misses a few opportunities to make the implementation more adaptable to changes in the future, Jake points out where he would have done things differently. Bill acknowledges that Jake’s suggestions would be better and would have probably been just as easy to implement from the beginning, but inwardly he’s a bit disappointed that Jake didn’t like his design as-is and that he has to do some rework. In the end, Bill is left with a feeling of critique rather than collaboration.</p> <p>Whether it’s a high-level UML diagram or working code, how one person tends to perceive feedback on the ideas comprising a potential solution has everything to do with timing. It can be the exact same feedback they would have received either way, but when the feedback occurs often makes a difference between whether it’s perceived as collaboration or critique. It’s all about when the conversation happens.</p>