<?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: ONDŘEJ PODHORNÝ</title>
    <description>The latest articles on DEV Community by ONDŘEJ PODHORNÝ (@ondej_podhorny).</description>
    <link>https://dev.to/ondej_podhorny</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%2F4142982%2F718877d7-7971-49bb-ad7f-11b711089022.png</url>
      <title>DEV Community: ONDŘEJ PODHORNÝ</title>
      <link>https://dev.to/ondej_podhorny</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ondej_podhorny"/>
    <language>en</language>
    <item>
      <title>EF Core vs Marten and Polecat</title>
      <dc:creator>ONDŘEJ PODHORNÝ</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:07:05 +0000</pubDate>
      <link>https://dev.to/ondej_podhorny/ef-core-vs-marten-and-polecat-1k9m</link>
      <guid>https://dev.to/ondej_podhorny/ef-core-vs-marten-and-polecat-1k9m</guid>
      <description>&lt;p&gt;EF Core is the .NET default long after the thing you are saving has stopped being a set of rows. An order with its lines, discounts, and address is one decision. The template still models it as a join.&lt;/p&gt;

&lt;p&gt;I reach for EF Core when the model is rows: a line that is queried without its parent, a report that joins five tables, a schema another service already reads with SQL. I reach for a document when that order is loaded and saved as one unit. The second choice does not require a second database. Marten stores the document as JSONB in the PostgreSQL you already run. Polecat is that document model on SQL Server. EF Core stays for the tables that are actually relational.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aggregate became a join
&lt;/h2&gt;

&lt;p&gt;The symptoms show up before anyone argues about databases. Loading one order walks every relationship the screen might need. &lt;code&gt;Include&lt;/code&gt; chains grow. &lt;code&gt;AsSplitQuery&lt;/code&gt; appears because one query returned a cartesian product. The Fluent API in &lt;code&gt;OnModelCreating&lt;/code&gt; holds the mapping the class no longer shows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LoadOrderEfCore.cs&lt;/strong&gt;&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Discounts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusHistory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSplitQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That query is honest about a relational model. It is a tax on a document. Early in a product, every new field on the aggregate wants a migration, and the migration is arguing with a schema the business has not settled. The mapping layer is there so the ORM can split a thing the domain keeps together.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ToJson&lt;/code&gt; puts an owned graph into a JSON column on that same row. The order is still a table, the mapping still lives in the model, and the change tracker still decides the save. The column got easier. The aggregate is still something the ORM assembled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default stays
&lt;/h2&gt;

&lt;p&gt;Three habits, and they reinforce each other.&lt;/p&gt;

&lt;p&gt;Official templates and most conference talks start from EF Core. It is familiar, it is staffable, and the ecosystem assumes it. Choosing it needs no meeting. Choosing anything else does.&lt;/p&gt;

&lt;p&gt;Most of us were trained to normalize first. Third normal form is the right reflex for an invoice line that accounting will sum without the order, and for a ledger. Applied to an aggregate, it produces the &lt;code&gt;Include&lt;/code&gt; chain above and calls that design.&lt;/p&gt;

&lt;p&gt;The alternative people picture is another cluster. MongoDB or DynamoDB, a second backup story, a second failure domain, and a dual write between the document and the relational system that still has to exist. That trade is real when the document cluster is the product. It is the wrong trade when the only goal was to stop joining an order to itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  One roundtrip, same engine
&lt;/h2&gt;

&lt;p&gt;The document session loads the order by id, the domain decides, and one commit writes the document back. Marten does this on PostgreSQL. Polecat does this on SQL Server. The table is &lt;code&gt;mt_doc_order&lt;/code&gt;. The body is JSONB, or JSON on SQL Server. There is no line-item table unless you create one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LoadOrderDocument.cs&lt;/strong&gt;&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&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;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coupon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Store&lt;/code&gt; is the upsert. The rules for &lt;code&gt;Insert&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, indexes, and a lost update are the document guide. A stream of facts, when you need the history and not only the current order, is &lt;a href="https://dev.to/guides/marten-event-sourcing"&gt;Marten Event Sourcing in Production&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;EF Core&lt;/th&gt;
&lt;th&gt;Marten / Polecat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tables and JOINs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;JSON document&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your Postgres or SQL Server&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your Postgres or SQL Server&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ACID across models&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes. Relational only.&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes. One transaction.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Includes and mapping&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Natural and typed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Schema friction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema changes need a migration.&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Low&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Low" is the document, not the database. A new property inside the JSON is not a new column. A computed index is cheap to add. A duplicated column, which a &lt;code&gt;DateTime&lt;/code&gt; filter needs, is still a migration, and production applies it before the new code serves traffic. The friction that goes away is the migration for every field of an aggregate that is still changing shape.&lt;/p&gt;

&lt;p&gt;Both columns say the same database because that is the point. You do not stand up MongoDB to get a document. Backups, restore drills, and the people who already run PostgreSQL or SQL Server stay the operational home. A dedicated document cluster still fits when the access pattern is an aggregation pipeline, sharding, and operators who already run it. Moving that workload onto Marten so the architecture diagram says Postgres trades a database the team knows for a JSONB schema that will not behave like that pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both models, one instance
&lt;/h2&gt;

&lt;p&gt;I do not throw EF Core away to adopt the document. One PostgreSQL or SQL Server instance holds both.&lt;/p&gt;

&lt;p&gt;On one side, Marten or Polecat: the aggregates that are loaded and saved whole, one roundtrip by id, &lt;code&gt;mt_doc_order&lt;/code&gt;. On the other, EF Core or Dapper: invoices, the general ledger, the report that joins tables, the legacy schema that already has a reader. A screen that wants five joins is telling you the data is relational. A command that loads one order to decide is telling you the data is a document.&lt;/p&gt;

&lt;p&gt;The table's "one transaction" means those two writes can share the database transaction you already have. It does not mean a distributed transaction between PostgreSQL and a second cluster, and it does not mean every command should touch both models. On PostgreSQL, a Marten session and an EF Core context can enlist in the same transaction when one request truly writes both. That request is rarer than a diagram suggests. If every command needs both, the boundary is wrong. The enlistment is also specific to that engine. I am not going to pretend a PostgreSQL transaction sample is the Polecat and SQL Server wiring.&lt;/p&gt;

&lt;p&gt;Polyglot, on this reading, is another model in the same engine. Strictly relational access stays on EF Core or Dapper. Time-series telemetry on PostgreSQL can be TimescaleDB beside Marten, still one cluster to back up. A second engine is a decision about operations, not a requirement of the document.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>MediatR vs. Wolverine: Architecture, Ergonomics, and Feature Matrix</title>
      <dc:creator>ONDŘEJ PODHORNÝ</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:49:10 +0000</pubDate>
      <link>https://dev.to/ondej_podhorny/mediatr-vs-wolverine-architecture-ergonomics-and-feature-matrix-7dm</link>
      <guid>https://dev.to/ondej_podhorny/mediatr-vs-wolverine-architecture-ergonomics-and-feature-matrix-7dm</guid>
      <description>&lt;p&gt;MediatR taught .NET teams to keep HTTP away from business logic. Wolverine asks whether that boundary still needs a marker interface.&lt;/p&gt;

&lt;p&gt;For years, MediatR has served as the default standard for implementing CQRS and in-process decoupling in .NET applications. It introduced clean boundary separation between HTTP controllers and business logic, providing an accessible, predictable structure for monolithic codebases.&lt;/p&gt;

&lt;p&gt;As distributed requirements, event-driven architectures, and performance constraints have matured, the limitations of simple in-memory mediator patterns have become clearer. Wolverine, part of the Critter Stack ecosystem, approaches this challenge from a fundamentally different perspective: replacing interface ceremony with code generation and integrating messaging capabilities directly into the framework core.&lt;/p&gt;

&lt;p&gt;This article breaks down the architectural, ergonomic, and operational differences between MediatR and Wolverine to evaluate when each tool makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ergonomics: interface ceremony vs. pure POCOs
&lt;/h2&gt;

&lt;p&gt;The structural difference is evident in handler and message definitions. MediatR enforces strict generic interfaces, whereas Wolverine relies on conventions and dynamic code generation to eliminate boilerplate.&lt;/p&gt;

&lt;h3&gt;
  
  
  MediatR: marker interfaces and rigid signatures
&lt;/h3&gt;

&lt;p&gt;In MediatR, every command, query, and handler must implement specific interfaces:&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="c1"&gt;// 1. Command requires marker interface&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Handler ceremony and constructor injection boilerplate&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderResult&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;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrderHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&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;CreateOrderHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateOrderHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&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;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrder&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order for {CustomerId}..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&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="nf"&gt;FromResult&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;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&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;This pattern introduces several architectural constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interface coupling.&lt;/strong&gt; Commands and handlers are coupled to &lt;code&gt;IRequest&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;IRequestHandler&amp;lt;TRequest, TResponse&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constructor clutter.&lt;/strong&gt; Dependencies require explicit constructor injection and private field assignments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forced asynchrony.&lt;/strong&gt; Handlers must return &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt; and accept a &lt;code&gt;CancellationToken&lt;/code&gt;, even when handling pure in-memory, synchronous computations. This forces artificial state machines and allocations via &lt;code&gt;Task.FromResult&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wolverine: convention over ceremony
&lt;/h3&gt;

&lt;p&gt;Wolverine strips away external interface constraints entirely:&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="c1"&gt;// 1. Clean, zero-interface C# POCO&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Static class and direct method injection (no constructor DI)&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderHandler&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;OrderResult&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateOrder&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;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order for {CustomerId}..."&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;CustomerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&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;Key ergonomic differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-interface messaging.&lt;/strong&gt; Messages remain pure POCOs without framework references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method-level dependency injection.&lt;/strong&gt; Handlers can be declared as static classes, with dependencies passed directly into the &lt;code&gt;Handle&lt;/code&gt; method parameters alongside the command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible signatures.&lt;/strong&gt; If an operation is synchronous, return the raw type directly. If a &lt;code&gt;CancellationToken&lt;/code&gt; is needed, declare it; if not, omit it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comprehensive feature matrix
&lt;/h2&gt;

&lt;p&gt;While MediatR focuses solely on in-process dispatching, Wolverine acts as both an in-memory mediator and an integration bus.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;MediatR&lt;/th&gt;
&lt;th&gt;Wolverine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope &amp;amp; Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal in-process mediator&lt;/td&gt;
&lt;td&gt;Full messaging &amp;amp; integration bus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team Onboarding &amp;amp; Ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢 De-facto industry standard&lt;/td&gt;
&lt;td&gt;🟡 Emerging, convention-heavy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coding Style&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IRequest&lt;/code&gt;, Handlers&lt;/td&gt;
&lt;td&gt;Zero-interface (POCO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP Endpoint Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Minimal API / MVC only&lt;/td&gt;
&lt;td&gt;✅ Wolverine.Http (zero MVC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cascading Messages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Explicit dispatch calls&lt;/td&gt;
&lt;td&gt;✅ Native return types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transactional Outbox/Inbox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ External libraries&lt;/td&gt;
&lt;td&gt;✅ EF Core, PG, SQL, Marten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Async Message Brokers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ In-process only&lt;/td&gt;
&lt;td&gt;✅ RabbitMQ, Kafka, ASB, SQS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry &amp;amp; Dead Letter Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Complex in behaviors&lt;/td&gt;
&lt;td&gt;✅ Per-exception &amp;amp; circuit breakers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stateful Sagas &amp;amp; Workflows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Not supported&lt;/td&gt;
&lt;td&gt;✅ First-class stateful sagas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry &amp;amp; Tracing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ Custom pipeline&lt;/td&gt;
&lt;td&gt;✅ Built-in natively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Native AOT / Cold Starts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢 Zero-overhead out-of-the-box&lt;/td&gt;
&lt;td&gt;🟡 Requires code pre-generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In-Memory Dispatch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Direct delegate invoke&lt;/td&gt;
&lt;td&gt;Full message envelope context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Architectural scope: beyond in-process dispatch
&lt;/h2&gt;

&lt;p&gt;MediatR stops at the process boundary. When requirements involve asynchronous background processing, external message queues, or reliable transaction boundaries, developers must build or integrate secondary frameworks (such as MassTransit or custom outbox implementations).&lt;/p&gt;

&lt;p&gt;Wolverine bridges this gap by unifying local invocation and external messaging into a single abstraction model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cascading messages via return types.&lt;/strong&gt; Handlers can return tuples, for example &lt;code&gt;(OrderResult Result, OrderPlaced Event)&lt;/code&gt;. Wolverine automatically sends the event downstream to message queues or outbox storage while returning the result to the direct caller, avoiding manual dispatcher calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native transactional outbox.&lt;/strong&gt; Ensures consistency between database writes and message broker dispatching natively, without third-party plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience and error handling.&lt;/strong&gt; Wolverine provides declarative retry policies, fallback logic, and dead-letter queues configured per message or exception type.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational trade-offs
&lt;/h2&gt;

&lt;p&gt;Choosing between these frameworks requires weighing specific architectural compromises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conventions vs. explicit types.&lt;/strong&gt; MediatR’s explicit interface hierarchies make code navigation straightforward via standard IDE tools. Wolverine’s convention-based discovery requires developers to understand its method naming and parameter binding rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup performance and AOT.&lt;/strong&gt; MediatR relies on standard DI service registration. Wolverine uses Lamar and runtime Roslyn compilation by default to maximize throughput. For environments requiring minimal cold-start latencies or Native AOT, teams must incorporate ahead-of-time code pre-generation into their build pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adoption and maintenance.&lt;/strong&gt; MediatR has an established track record and extensive documentation across the enterprise ecosystem. Wolverine offers modern ergonomics and broader built-in capabilities, but onboarding requires familiarity with convention-driven patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which should you choose?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose MediatR&lt;/strong&gt; if your application is a modular monolith requiring strict in-memory command dispatching, explicit interface-driven navigation, and zero additional infrastructure dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Wolverine&lt;/strong&gt; if your application needs an integrated messaging pipeline with transactional outbox support, message broker integration, minimal ceremony, and declarative resilience built directly into the runtime.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;These architectural trade-offs, combined with Marten as a document database on PostgreSQL, were the primary reason I decided to rebuild the backend of Codebase Pulse on top of the Critter Stack.&lt;/em&gt;&lt;/p&gt;

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