<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Malcolm</title>
    <description>The latest articles on DEV Community by Malcolm (@malcolmmcneely).</description>
    <link>https://dev.to/malcolmmcneely</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3963340%2F8732d342-40e3-4917-a3c5-5fcf049fe925.png</url>
      <title>DEV Community: Malcolm</title>
      <link>https://dev.to/malcolmmcneely</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/malcolmmcneely"/>
    <language>en</language>
    <item>
      <title>AI agentic workflows on large codebases</title>
      <dc:creator>Malcolm</dc:creator>
      <pubDate>Tue, 09 Jun 2026 06:54:49 +0000</pubDate>
      <link>https://dev.to/malcolmmcneely/ai-agentic-workflows-on-large-codebases-3275</link>
      <guid>https://dev.to/malcolmmcneely/ai-agentic-workflows-on-large-codebases-3275</guid>
      <description>&lt;p&gt;The &lt;a href="https://malcolmania.co.uk/writing/edict" rel="noopener noreferrer"&gt;first post&lt;/a&gt; went over some of its capabilities. Over the past week Edict went v1.0, adding cursors for reading projections after command dispatch (to close some eventual-consistency gaps), a new type of projection that holds state inside the Orleans grain directly instead of a table, saga timeouts, schedules, an improved skills package and MCP server that ships with Edict, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/MalcolmMcNeely/Edict" rel="noopener noreferrer"&gt;Edict&lt;/a&gt; has now grown to over 75,000 lines of code and more than 1000 tests, and contains several deep mechanisms that have been fixed, broken, and fixed again. It is well past the point where I can hold all of Edict in my head.&lt;/p&gt;

&lt;p&gt;This post is about working with AI on large codebases, which I expect to be the first problem most software engineers have to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The context problem
&lt;/h2&gt;

&lt;p&gt;Years ago I was talking to a PhD candidate whose area of research was Natural Language Processing (NLP). He explained to me that one of the most difficult NLP problems was context. If a colleague says they need to pop out to pick their kids up from school, a scene can form in your head: one with a school, the layout of the road, people waiting, walking, driving, the environs. You may never have seen the school your colleague mentioned, but you can form a rich scene from your accumulated experience and use it to drive the rest of the conversation with a shared understanding.&lt;/p&gt;

&lt;p&gt;LLMs ingeniously dodge this entire issue by making it your problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just a word-probability machine
&lt;/h2&gt;

&lt;p&gt;Strip away the chat window and a Large Language Model (LLM) is doing one thing: predicting the next token. Give it a run of text and it returns a probability distribution over what comes next, samples one, appends it, and repeats.&lt;/p&gt;

&lt;p&gt;Companies like OpenAI and Anthropic then beat it into shape using techniques like supervised fine-tuning and reinforcement learning, which tune those probabilities in meaningful ways.&lt;/p&gt;

&lt;p&gt;That is why Claude is always telling me "Good framing" or "You've spotted...". It even called me "Bold" on one occasion. The probabilities have been shaped to reward &lt;em&gt;me&lt;/em&gt;; in other words the predictor is being steered towards outputs deemed preferable to people. This includes a proclivity to always complete a given task, making assumptions to do so, which can be dangerous when writing code.&lt;/p&gt;

&lt;p&gt;What matters here is knowing where the gap is between you and the LLM, and how &lt;a href="https://arxiv.org/html/2605.08837v1" rel="noopener noreferrer"&gt;they are not the same thing&lt;/a&gt;. What it builds is statistical. When your colleague gave you, the human, the input of the school run, you assembled a scene out of a life of standing at school gates. When an LLM is given the same input as text, it assembles associations from text about school runs. Models lean on word association and underproduce exactly the emotional and physical detail a person supplies for free.&lt;/p&gt;

&lt;p&gt;That is why it lays context at your feet: it cannot fetch yours, so you have to hand it over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Side note: affectations and non-determinism
&lt;/h3&gt;

&lt;p&gt;Because the interface is natural language, it is tempting to talk to an LLM like a person. I have found that effective, but two things are worth knowing.&lt;/p&gt;

&lt;p&gt;First, non-determinism: the same prompt can give you different answers on different runs. It is a probability engine, not a lookup. For a codebase that means you cannot assume yesterday's good result repeats exactly.&lt;/p&gt;

&lt;p&gt;Second, and more useful day to day: the model is acutely sensitive to how you frame things. "My CTO recommended this technology" or "I'm sceptical this will work" can produce entirely different outcomes, because the model tends to mirror the stance you hand it. The trap is telegraphing the answer you want. I stay factual and state goals plainly, not because emotion is forbidden, but because it stops me biasing the reply. It is part of why &lt;a href="https://developers.redhat.com/articles/2025/10/22/how-spec-driven-development-improves-ai-coding-quality#how_to_get_started_with_spec_coding" rel="noopener noreferrer"&gt;spec-driven development&lt;/a&gt; works so well: a spec is intent stated without a thumb on the scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The purpose of an agentic workflow is context
&lt;/h2&gt;

&lt;p&gt;The workflow I use is largely based on &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's skills&lt;/a&gt; with a few tweaks. There is a lot of great stuff in there, but the absolute minimum I use is four commands, run in order.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/grill-with-docs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is where the context is established. It does four things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads and maintains a &lt;code&gt;CONTEXT.md&lt;/code&gt;&lt;/strong&gt; (or a &lt;code&gt;CONTEXT-MAP.md&lt;/code&gt; plus project-specific &lt;code&gt;CONTEXT.md&lt;/code&gt;s for larger solutions), which holds your domain and its relationships. If you look at &lt;a href="https://github.com/MalcolmMcNeely/Edict/blob/main/CONTEXT.md" rel="noopener noreferrer"&gt;Edict's &lt;code&gt;CONTEXT.md&lt;/code&gt;&lt;/a&gt; and are familiar with CQRS / event-driven systems, none of it should surprise you. But this is how I hand my context to every Claude session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads and maintains Architectural Decision Records (ADRs)&lt;/strong&gt; for non-obvious, hard-to-reverse decisions. This is how we address bad assumptions made by the LLM. &lt;a href="https://github.com/MalcolmMcNeely/Edict/blob/main/docs/adr/0051-eventid-assigned-once-at-enqueue.md" rel="noopener noreferrer"&gt;ADR 0051&lt;/a&gt;, for example, details how event IDs are stamped once at enqueue time and persist through the idempotency, claim-check, and outbox mechanisms. Changing event IDs would cause havoc for Edict's telemetry and break several mechanisms, including the idempotency layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walks Edict's surfaces&lt;/strong&gt; (my addition). Across 45 projects, Edict has:

&lt;ul&gt;
&lt;li&gt;Source generation&lt;/li&gt;
&lt;li&gt;Roslyn analysers&lt;/li&gt;
&lt;li&gt;Benchmarks for performance regression&lt;/li&gt;
&lt;li&gt;A skills package and MCP tooling for other developers' LLM sessions&lt;/li&gt;
&lt;li&gt;An in-memory testing library, &lt;code&gt;Edict.Testing&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A gold-standard sample app demonstrating every feature of Edict, including the use of &lt;code&gt;Edict.Testing&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At some point it became a nightmare to keep everything aligned, and I found that adding this as another step kept all my ducks in a row.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Walks every branch of the decision tree.&lt;/strong&gt; This is the essential guard against the LLM making bad guesses and assumptions. A mistake made here snowballs into every later step, as well as into future work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/to-prd&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Takes the context you established with &lt;code&gt;/grill-with-docs&lt;/code&gt;, including every decision you made, analyses it along several axes (schema changes, architectural changes), and works out the implementation details while respecting your domain language and any relevant ADRs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/to-issues&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Takes the resulting Product Requirements Document (PRD) and turns it into vertical slices ready for implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/tdd&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Takes a vertical slice and implements it using a red-green-refactor method. &lt;a href="https://www.youtube.com/watch?v=hYZdIwFIy-c" rel="noopener noreferrer"&gt;Matt has an excellent video&lt;/a&gt; on why this works so well with LLMs.&lt;/p&gt;

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

&lt;p&gt;At first it might be tempting to think "I need a better prompt", and that works for small projects. It does not scale to enterprise systems, or even a mere 75,000 lines of code.&lt;/p&gt;

&lt;p&gt;If you accept what an LLM is, a powerful predictor with no access to the scene in your head, the problem shifts: how do you carry your context across every session, for an entire codebase? &lt;code&gt;CONTEXT.md&lt;/code&gt; gives it the domain. ADRs give it the decisions it would otherwise guess at. Walking the surfaces and the decision tree catches the bad assumptions before they snowball. PRD, issues, and TDD turn that shared understanding into code, in slices small enough to review (if that's your thing).&lt;/p&gt;

&lt;p&gt;The vocabulary differs, but &lt;a href="https://github.blog/ai-and-ml/github-copilot/how-to-build-reliable-ai-workflows-with-agentic-primitives-and-context-engineering/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;, &lt;a href="https://martinfowler.com/articles/exploring-gen-ai/context-engineering-coding-agents.html" rel="noopener noreferrer"&gt;Thoughtworks&lt;/a&gt;, and many more are all converging on this concept of Context Engineering: spending human effort up-front to establish durable context and constraints so LLMs can take the wheel without drifting into dangerous territory.&lt;/p&gt;

&lt;p&gt;Edict is past the point where I can hold it in my head. The workflow is how I hold it instead. The model never understands the codebase the way I do, and it does not need to, as long as I keep laying the context at its feet.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Edict: a CQRS framework for .NET, built in two weeks with Claude</title>
      <dc:creator>Malcolm</dc:creator>
      <pubDate>Tue, 02 Jun 2026 11:12:38 +0000</pubDate>
      <link>https://dev.to/malcolmmcneely/edict-a-cqrs-framework-for-net-built-in-two-weeks-with-claude-3i09</link>
      <guid>https://dev.to/malcolmmcneely/edict-a-cqrs-framework-for-net-built-in-two-weeks-with-claude-3i09</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Most of my career was making things the same."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A staff engineer I worked with said that to me once. He was talking about a different framework at the time, but it was exactly the kind of problem &lt;strong&gt;Edict&lt;/strong&gt; solves. That conversation stuck. It was a big driver in building this.&lt;/p&gt;

&lt;p&gt;Edict is a CQRS framework for .NET on top of Microsoft Orleans. It absorbs the plumbing every event-driven team rewrites by hand: idempotency keys for at-least-once redeliveries, an outbox for atomic state and events, trace propagation across stream hops, a queryable dead-letter projection.&lt;/p&gt;

&lt;p&gt;I built it in two weeks. Claude wrote almost every line of code; I drove the design, reviewed every change, and corrected course when needed.&lt;/p&gt;

&lt;p&gt;This post is the short tour. What it does, how it was built, whether it's worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this was built
&lt;/h2&gt;

&lt;p&gt;Two weeks of focused sessions, almost entirely agentic. Claude wrote the code; I drove the design and reviewed every change.&lt;/p&gt;

&lt;p&gt;The workflow was loosely modelled on &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's skills&lt;/a&gt;: each feature began as a PRD on the issue tracker, got broken into vertical slices, and landed via red-green-refactor TDD. Domain language lives in &lt;code&gt;CONTEXT.md&lt;/code&gt;; load-bearing decisions live in ADRs. Whenever I caught myself making the same correction twice, I codified it as a project skill or analyzer.&lt;/p&gt;

&lt;p&gt;This is not "look what AI can do alone." Claude is a powerful implementation surface but it needed a human with strong architectural opinions and a clear domain language to be useful. This is what agentic development looks like when the human has done the design work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/orleans/" rel="noopener noreferrer"&gt;Microsoft Orleans&lt;/a&gt; is great. It gives you a programming model where every entity in your system has exactly one in-memory home, on one node, on one thread at a time. The whole class of &lt;em&gt;"two pods, same order, race condition"&lt;/em&gt; problems just disappears.&lt;/p&gt;

&lt;p&gt;But Orleans is a runtime, not an opinion. The moment you want CQRS, event-driven flows, sagas, projections, or an outbox, you start writing the same plumbing every team writes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency keys for at-least-once redeliveries&lt;/li&gt;
&lt;li&gt;Trace propagation across async stream hops&lt;/li&gt;
&lt;li&gt;Atomic commit of state &lt;em&gt;and&lt;/em&gt; the events you raised&lt;/li&gt;
&lt;li&gt;A dead-letter table for the poison message that just took out your aggregate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it is hard. All of it is repetitive. And most teams get at least one of them subtly wrong.&lt;/p&gt;

&lt;p&gt;Edict's bet is that this is a framework's job, not yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it feels like
&lt;/h2&gt;

&lt;p&gt;A command handler is one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderCommandHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EdictCommandHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EdictCommandResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlaceOrderCommand&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Raise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderPlacedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EdictCommandResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EdictCommandResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Accepted&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A validator that gates that handler is one constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPlaceCommandValidator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EdictCommandValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PlaceOrderCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderPlaceCommandValidator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerReference&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;WithErrorCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer_reference_required"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs in the same activation turn as the handler, before &lt;code&gt;HandleAsync&lt;/code&gt;. A failure short-circuits to &lt;code&gt;EdictCommandResult.Rejected&lt;/code&gt; with &lt;code&gt;customer_reference_required&lt;/code&gt; as the rejection code; the handler never sees an invalid command and no state mutation occurs.&lt;/p&gt;

&lt;p&gt;An event handler is also one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderEmailHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEmailSender&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EdictEventHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderPlacedEvent&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendConfirmation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides of an event-driven flow. No Orleans interfaces. No stream wiring. No serialization attributes. No idempotency code. Source generators connect &lt;code&gt;HandleAsync&lt;/code&gt; to the right stream based on its parameter type, and the base class deduplicates redeliveries by &lt;code&gt;EventId&lt;/code&gt; before ever calling you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole vocabulary
&lt;/h2&gt;

&lt;p&gt;There are six things you write. That's it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What you write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Command handler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The aggregate's invariant. Receives a command, mutates state, raises events.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event handler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A side effect. Receives an event, does something (send email, call API).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Saga&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A long-running coordinator. Reacts to events, sends commands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Projection builder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A read model. Receives events, writes a queryable row.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sender&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How callers reach into the system to issue a command.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stream&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A topic identity. Where events flow.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything else is the framework's problem: routing, serialization, the outbox, retries, dead-lettering, tracing, the parts of Orleans you'd rather not type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow end to end
&lt;/h2&gt;

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

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

&lt;p&gt;One OpenTelemetry trace covers the whole graph. If any handler throws, the failure lands in a dead-letter projection you can query. The aggregate keeps accepting commands.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Pick your substrate
&lt;/h2&gt;

&lt;p&gt;The same handler code runs on either of two reference pairings, both passing the same conformance battery:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Substrate&lt;/th&gt;
&lt;th&gt;Streaming&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Azure Queue Storage&lt;/td&gt;
&lt;td&gt;Azure Table Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kafka + Postgres&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache Kafka&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Adding a third is a matter of implementing the substrate seam. The framework itself doesn't care which queue or store sits underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing without containers
&lt;/h2&gt;

&lt;p&gt;Edict ships an in-memory test app so you can exercise an entire command → event → saga → projection flow in-process. No Orleans cluster, no Azurite, no Docker. Three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;EdictTestApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderCommandHandler&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PlaceOrderCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"REF-001"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSagaProgress&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPaymentSaga&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderPaymentProgress&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Chaos is on by default.&lt;/strong&gt; Duplicate redeliveries and bounded reorder are simulated deterministically, so every test you write exercises the at-least-once guarantees production has to tolerate. No setup required.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-assisted development, built in
&lt;/h2&gt;

&lt;p&gt;Edict's whole philosophy is that the framework should absorb the things every team rewrites by hand, so feature devs can focus on feature code. The MCP server and Claude Code skill bundle that ship with Edict apply that same principle to AI tooling: consumers should be able to use Claude productively against Edict without first writing scaffolding to teach the agent what Edict is.&lt;/p&gt;

&lt;p&gt;Ask Claude &lt;em&gt;"where does &lt;code&gt;PlaceOrderCommand&lt;/code&gt; get routed?"&lt;/em&gt; and it calls &lt;code&gt;edict_describe_silo_wiring&lt;/code&gt; instead of guessing from grep results. Ask it to add a saga and it calls &lt;code&gt;edict_list_route_keys&lt;/code&gt; to see which &lt;code&gt;RouteKey&lt;/code&gt; Guids are already taken, so it generates a fresh one instead of colliding. Ask it why a dead-letter behaves a particular way and it calls &lt;code&gt;edict_lookup_adr&lt;/code&gt; and returns the source decision.&lt;/p&gt;

&lt;p&gt;The two pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;edict-mcp&lt;/code&gt;&lt;/strong&gt; is an MCP server that exposes six tools the agent can call against your live solution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;edict-skills&lt;/code&gt;&lt;/strong&gt; is a Claude Code skill bundle that knows &lt;em&gt;when&lt;/em&gt; to call each tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together they mean the agent works from your actual code, not from a guess about what an event-driven framework probably looks like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill (when it fires)&lt;/th&gt;
&lt;th&gt;What the agent stops guessing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;edict-authoring&lt;/strong&gt; (adding a handler, saga, or projection)&lt;/td&gt;
&lt;td&gt;Which &lt;code&gt;RouteKey&lt;/code&gt; Guids are taken, which handlers already exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;edict-silo-wiring&lt;/strong&gt; (touching any &lt;code&gt;AddEdict*&lt;/code&gt; call)&lt;/td&gt;
&lt;td&gt;Which substrate is wired in &lt;code&gt;Program.cs&lt;/code&gt;, which extensions are missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;edict-contracts&lt;/strong&gt; (attribute or wire-format questions)&lt;/td&gt;
&lt;td&gt;What a &lt;code&gt;Stream&lt;/code&gt; is, why &lt;code&gt;[Union]&lt;/code&gt; is banned (with the source ADR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;edict-diagnostics&lt;/strong&gt; (debugging dead-letter, outbox, or trace issues)&lt;/td&gt;
&lt;td&gt;Why the framework behaves the way it does, with the decision record attached&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I think this is going to matter more over the next year, not less. The frameworks that work well with AI tools are the ones that tell the agent the truth about themselves. That's a lot easier to do for the framework author than for every consumer team to invent on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it fits
&lt;/h2&gt;

&lt;p&gt;Edict is probably worth a look if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're already on Orleans, or you've been weighing it up&lt;/li&gt;
&lt;li&gt;You want CQRS and event-driven flows without handwriting the plumbing&lt;/li&gt;
&lt;li&gt;You like writing C# and want one programming model across every entity in the system&lt;/li&gt;
&lt;li&gt;You're curious what agentic development can produce when the human in the loop knows the design space cold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's probably &lt;strong&gt;not&lt;/strong&gt; for you if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A REST API over EF Core is enough. You genuinely don't need this.&lt;/li&gt;
&lt;li&gt;You can't move to .NET 10&lt;/li&gt;
&lt;li&gt;You need a battle-tested production framework today. Edict is portfolio quality. The design is solid and the test coverage is real, but no one is running it at a Fortune 500 yet, including me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather you know that up front than find out later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/MalcolmMcNeely/Edict" rel="noopener noreferrer"&gt;https://github.com/MalcolmMcNeely/Edict&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting started:&lt;/strong&gt; &lt;a href="https://github.com/MalcolmMcNeely/Edict/blob/main/docs/usage/getting-started.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/usage/getting-started.md&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Sample app:&lt;/strong&gt; clone the repo, run the Aspire AppHost, and watch commands flow through to a Blazor dashboard in real time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reasoning:&lt;/strong&gt; every non-obvious design decision lives in &lt;a href="https://github.com/MalcolmMcNeely/Edict/tree/main/docs/adr" rel="noopener noreferrer"&gt;&lt;code&gt;docs/adr/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spot something missing, surprising, or wrong, a GitHub issue is the fastest way to reach me. I'm also &lt;a href="https://www.linkedin.com/in/malcolm-mcneely-programmer" rel="noopener noreferrer"&gt;on LinkedIn&lt;/a&gt;.&lt;/p&gt;

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

</description>
      <category>dotnet</category>
      <category>ai</category>
      <category>cqrs</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
