<?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.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>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>
