<?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>SQL Server in Docker &amp; .NET Aspire: Zero-Pollution Local Dev Experience</title>
      <dc:creator>ONDŘEJ PODHORNÝ</dc:creator>
      <pubDate>Tue, 03 Nov 2026 11:57:01 +0000</pubDate>
      <link>https://dev.to/ondej_podhorny/sql-server-in-docker-net-aspire-zero-pollution-local-dev-experience-2ac2</link>
      <guid>https://dev.to/ondej_podhorny/sql-server-in-docker-net-aspire-zero-pollution-local-dev-experience-2ac2</guid>
      <description>&lt;p&gt;Surprisingly many .NET developers still don't know SQL Server runs natively in Docker containers. Microsoft has officially supported SQL Server in optimized Linux containers for years — and paired with .NET Aspire, it eliminates host OS pollution and revolutionizes local developer experience.&lt;/p&gt;

&lt;p&gt;For over a decade, the standard onboarding routine for a .NET developer working with SQL Server was downloading a multi-gigabyte installer, setting up a local SQL Server Express or Developer instance, and leaving behind dangling Windows services, registry keys, and shared DLL bloat.&lt;/p&gt;

&lt;p&gt;Running SQL Server in Docker completely flips this model: instant local provisioning, zero machine pollution, and identical behavior on developer laptops and CI/CD runners.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision &amp;amp; Evaluation Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload &amp;amp; Environment&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Practical Rationale &amp;amp; Architecture Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Dev &amp;amp; Onboarding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Optimal&lt;/td&gt;
&lt;td&gt;Zero installation friction. New devs run &lt;code&gt;docker run&lt;/code&gt; or launch .NET Aspire and are coding in 60 seconds with no host pollution.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automated CI/CD Pipelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Optimal&lt;/td&gt;
&lt;td&gt;Spin up disposable database instances for each test job. Completely isolated state, 100% test reproducibility.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ephemeral Feature Branches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Optimal&lt;/td&gt;
&lt;td&gt;Preview environments in Kubernetes or local Docker can spin up dedicated databases per pull request and tear them down on merge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heavy Production Workloads&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Evaluation&lt;/td&gt;
&lt;td&gt;Fully supported on Linux/Kubernetes, but requires proper volume mount tuning (I/O latency) and standard commercial core licensing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Quick Start: One-line Local Setup
&lt;/h2&gt;

&lt;p&gt;A complete, fully functional SQL Server 2025 instance can be spun up on your machine in seconds without touching your host OS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"ACCEPT_EULA=Y"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"MSSQL_SA_PASSWORD=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SA_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;YourStrong&lt;/span&gt;&lt;span class="p"&gt;@Password123&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 1433:1433 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; sqlserver &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; mcr.microsoft.com/mssql/server:2025-latest

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starts in Seconds:&lt;/strong&gt; Microsoft's official Linux image (&lt;code&gt;[mcr.microsoft.com/mssql/server:2025-latest](https://mcr.microsoft.com/mssql/server:2025-latest)&lt;/code&gt;) boots fast and contains the full database engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port 1433:&lt;/strong&gt; Standard connection port, accessible via SQL Server Management Studio (SSMS), Azure Data Studio, JetBrains Rider, or the VS Code MSSQL extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic SA Password:&lt;/strong&gt; Reads &lt;code&gt;${SA_PASSWORD}&lt;/code&gt; if defined in your shell environment, with a safe fallback for local development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Host Contamination:&lt;/strong&gt; Clean teardown; running &lt;code&gt;docker rm -f sqlserver&lt;/code&gt; removes every trace of the database without residual registry keys or background services.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. First-Class .NET Aspire Integration
&lt;/h2&gt;

&lt;p&gt;While raw Docker commands or Docker Compose files work, &lt;a href="https://learn.microsoft.com/en-us/dotnet/aspire/" rel="noopener noreferrer"&gt;.NET Aspire&lt;/a&gt; elevates containerized SQL Server into an orchestrated, zero-config development workflow.&lt;/p&gt;

&lt;p&gt;In your Aspire &lt;code&gt;AppHost&lt;/code&gt; project, you declare SQL Server as a managed resource:&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;sqlserver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqlserver"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithImageTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2025-latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDataVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqlserver-data"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLifetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContainerLifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&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;sqlserver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"appdb"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Microservice gets connection string injected automatically&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Aspire Beats Raw Docker Compose
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-injected Secrets &amp;amp; Connections:&lt;/strong&gt; No manual password management or connection strings in &lt;code&gt;appsettings.Development.json&lt;/code&gt;. Aspire injects the connection string into dependent projects via environment variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Data Volume:&lt;/strong&gt; With &lt;code&gt;WithDataVolume("sqlserver-data")&lt;/code&gt; and &lt;code&gt;WithLifetime(ContainerLifetime.Persistent)&lt;/code&gt;, container restarts do not wipe your database tables or seed data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Dashboard:&lt;/strong&gt; Live resource logs, container health status, and OpenTelemetry metrics directly in the Aspire developer portal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The True F5 Workflow:&lt;/strong&gt; Hitting Run / Debug in your IDE automatically provisions the container, validates connection readiness, and boots your microservices in order.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The Trade-offs: Clear Upsides vs. Practical Catches
&lt;/h2&gt;

&lt;p&gt;Containerizing SQL Server provides immense velocity, but engineering teams must understand the practical trade-offs:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Clear Upsides
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instant Provisioning:&lt;/strong&gt; Spin up fresh DBs in seconds for dev or CI integration tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Host Contamination:&lt;/strong&gt; Clean teardown; no registry keys, dangling Windows background services, or DLL bloat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducible CI/CD:&lt;/strong&gt; Identical database engine behavior on developer laptops, Linux workstations, and GitHub Actions runners.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Practical Catches
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OS Architecture &amp;amp; I/O Overhead:&lt;/strong&gt; When running Docker on Windows (WSL2) or macOS (Docker Desktop / Colima), SQL Server executes within a lightweight Linux VM. While memory and CPU execution are virtually native, I/O across host-mounted file systems can incur translation overhead. Always use &lt;strong&gt;Docker named volumes&lt;/strong&gt; (e.g. &lt;code&gt;sqlserver-data&lt;/code&gt;) rather than host directory bind mounts (&lt;code&gt;-v /c/mydata:/var/opt/mssql/data&lt;/code&gt;) for optimal disk performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing Realities:&lt;/strong&gt; The official image defaults to &lt;strong&gt;Developer Edition, which is 100% free&lt;/strong&gt; for development, testing, and staging (with full Enterprise Edition feature parity). But deploying that container to production or running live business workloads requires standard Microsoft core licenses (&lt;code&gt;MSSQL_PID=Enterprise&lt;/code&gt; or &lt;code&gt;MSSQL_PID=Standard&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling Differences:&lt;/strong&gt; Linux containers need Linux CLI commands (&lt;code&gt;sqlcmd&lt;/code&gt;, &lt;code&gt;systemd&lt;/code&gt; coredumps) for OS-level diagnostics rather than traditional Windows PerfMon or Windows Event Viewer.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;Running SQL Server in Docker with .NET Aspire is a total game-changer for developer experience, local isolation, and CI/CD pipelines. Installing gigabytes of SQL Server directly onto developer workstations should be considered obsolete.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>sqlserver</category>
      <category>docker</category>
      <category>aspire</category>
    </item>
    <item>
      <title>Polecat on SQL Server 2025: Production Spike Migration</title>
      <dc:creator>ONDŘEJ PODHORNÝ</dc:creator>
      <pubDate>Tue, 03 Nov 2026 11:52:48 +0000</pubDate>
      <link>https://dev.to/ondej_podhorny/polecat-on-sql-server-2025-production-spike-migration-1lb</link>
      <guid>https://dev.to/ondej_podhorny/polecat-on-sql-server-2025-production-spike-migration-1lb</guid>
      <description>&lt;p&gt;For .NET teams tied to Microsoft SQL Server who want the developer velocity of document databases and Wolverine messaging, Polecat on SQL Server 2025 is a massive leap forward. Here is what we learned from migrating a production spike.&lt;/p&gt;

&lt;p&gt;If your team is committed to PostgreSQL, &lt;a href="https://martendb.io/" rel="noopener noreferrer"&gt;Marten&lt;/a&gt; remains the gold standard of .NET document databases and event sourcing. But what about enterprise environments where SQL Server is mandatory? Until SQL Server 2025 introduced native binary JSON (&lt;code&gt;JSON_TYPE&lt;/code&gt;, JSON indexes), running a high-performance document store on SQL Server felt like an uphill battle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://polecat.jasperfx.net/" rel="noopener noreferrer"&gt;Polecat&lt;/a&gt; brings the Critter Stack document model directly to SQL Server 2025. In our production spike migration, it delivered where it counts. But moving between the two engines reveals distinct architectural refinements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Spike Migration Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subsystem&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Spike Finding &amp;amp; Architecture Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL Server 2025 Aspire Host&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ OK&lt;/td&gt;
&lt;td&gt;Smooth local orchestration out of the box via .NET Aspire container images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WolverineFx.Polecat Outbox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ OK&lt;/td&gt;
&lt;td&gt;Transactional outbox pattern and message dispatch swapped with zero friction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LINQ Query Suite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Refactor&lt;/td&gt;
&lt;td&gt;Predicates require concrete members; interface-constrained filters throw &lt;code&gt;BadLinqExpressionException&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DI Scopes (FastEndpoints)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Refactor&lt;/td&gt;
&lt;td&gt;Sessions implement only &lt;code&gt;IAsyncDisposable&lt;/code&gt;; synchronous &lt;code&gt;CreateScope()&lt;/code&gt; fails on &lt;code&gt;Dispose&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch Queries (&lt;code&gt;IBatchedQueryable&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Refactor&lt;/td&gt;
&lt;td&gt;Does not inherit from &lt;code&gt;IQueryable&lt;/code&gt;; &lt;code&gt;Skip&lt;/code&gt;, &lt;code&gt;Take&lt;/code&gt;, and &lt;code&gt;OrderBy&lt;/code&gt; must run as standalone queries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;While the matrix above highlights high-level audit results, the real insights emerged in the implementation details. Moving our production spike across database boundaries forced us to confront strict runtime constraints, query compilation subtleties, and architectural refinements between the two engines.&lt;/p&gt;

&lt;p&gt;Here is the deep-dive into each technical finding and how we resolved it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Strict Asynchrony &amp;amp; DI Synchronous Scope Trap
&lt;/h2&gt;

&lt;p&gt;One of the most critical runtime differences between Marten and Polecat lies in lifecycle disposal and asynchrony.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marten:&lt;/strong&gt; Supported synchronous execution (&lt;code&gt;.ToList()&lt;/code&gt;) and hybrid disposable sessions implementing both &lt;code&gt;IDisposable&lt;/code&gt; and &lt;code&gt;IAsyncDisposable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polecat:&lt;/strong&gt; &lt;code&gt;System.NotSupportedException&lt;/code&gt; — Polecat strictly does not support synchronous LINQ enumeration. You must use &lt;code&gt;ToListAsync(...)&lt;/code&gt;. Furthermore, &lt;code&gt;IQuerySession&lt;/code&gt; and &lt;code&gt;IDocumentSession&lt;/code&gt; implement &lt;strong&gt;only `IAsyncDisposable&lt;/strong&gt;`.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The Real-World Trap (FastEndpoints &amp;amp; ASP.NET DI):&lt;/span&gt;
&lt;span class="c1"&gt;// Synchronous scope creation fails on Dispose:&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;scope&lt;/span&gt; &lt;span class="p"&gt;=&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;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&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;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IQuerySession&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Throws on scope exit:&lt;/span&gt;
&lt;span class="c1"&gt;// System.InvalidOperationException: 'QuerySession' type only implements IAsyncDisposable.&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Workaround / Fix:&lt;/span&gt;
&lt;span class="c1"&gt;// In hosted services and startup paths, transition to CreateAsyncScope:&lt;/span&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;asyncScope&lt;/span&gt; &lt;span class="p"&gt;=&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;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;asyncSession&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IQuerySession&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Legacy disposal note:&lt;/strong&gt; If you have legacy infrastructure or libraries calling synchronous &lt;code&gt;Dispose()&lt;/code&gt;, wrap the session in a &lt;code&gt;DispatchProxy&lt;/code&gt; forwarding &lt;code&gt;Dispose()&lt;/code&gt; to &lt;code&gt;DisposeAsync().GetAwaiter().GetResult()&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Narrower LINQ Expression Parser (&lt;code&gt;BadLinqExpressionException&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Marten’s LINQ provider has enjoyed over 10 years of edge-case normalization. Polecat’s &lt;code&gt;WhereClauseParser&lt;/code&gt; is leaner and stricter: it requires predicates to directly compare a &lt;strong&gt;concrete document member against a constant or value&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="c1"&gt;// ❌ Generic filter extensions constrained by an interface:&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;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ApplyScopeFilters&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&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;tenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IScopeFilterable&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&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;TenantId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Throws in Polecat:&lt;/span&gt;
&lt;span class="c1"&gt;// Polecat.Linq.BadLinqExpressionException: neither side resolves to a document member compared against a constant.&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Fix: Constrain to concrete base class or concrete overloads&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;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ApplyScopeFilters&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&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;tenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DocumentBase&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&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;TenantId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tenant&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;h2&gt;
  
  
  3. Batch Querying &amp;amp; &lt;code&gt;IBatchedQueryable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Both libraries provide &lt;code&gt;session.CreateBatchQuery()&lt;/code&gt;, combining multiple independent queries into a single database roundtrip.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marten (Mature):&lt;/strong&gt; &lt;code&gt;IBatchedQueryable&amp;lt;T&amp;gt;&lt;/code&gt; &lt;strong&gt;inherits from `IQueryable&lt;/strong&gt;&lt;code&gt; (&lt;/code&gt;Where&lt;code&gt;, &lt;/code&gt;OrderBy&lt;code&gt;, &lt;/code&gt;Skip&lt;code&gt;, &lt;/code&gt;Take&lt;code&gt;, &lt;/code&gt;Select&lt;code&gt;, &lt;/code&gt;Stats`).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polecat (Native-AOT Lean):&lt;/strong&gt; &lt;code&gt;IBatchedQueryable&amp;lt;T&amp;gt;&lt;/code&gt; &lt;strong&gt;does NOT inherit from `IQueryable&lt;/strong&gt;`. It exposes a lean interface tailored strictly for batch retrieval:
&lt;/li&gt;
&lt;/ul&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IBatchedQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IBatchedQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;);&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;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;();&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;();&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;T&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FirstOrDefault&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;h3&gt;
  
  
  Impact on Migration
&lt;/h3&gt;

&lt;p&gt;Extension methods written on &lt;code&gt;IQueryable&amp;lt;T&amp;gt;&lt;/code&gt; will not compile on &lt;code&gt;batch.Query&amp;lt;T&amp;gt;()&lt;/code&gt;. Paged or projected queries requiring &lt;code&gt;OrderBy&lt;/code&gt;, &lt;code&gt;Skip&lt;/code&gt;, &lt;code&gt;Take&lt;/code&gt;, or &lt;code&gt;Select&lt;/code&gt; must run as standalone async queries outside the batch.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. API Surface &amp;amp; Architecture Differences
&lt;/h2&gt;

&lt;p&gt;Polecat modernizes several legacy patterns accumulated by Marten:&lt;/p&gt;

&lt;h3&gt;
  
  
  Namespace Separation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marten:&lt;/strong&gt; Extension methods like &lt;code&gt;.ToListAsync()&lt;/code&gt; or &lt;code&gt;CreateBatchQuery()&lt;/code&gt; lived in the monolithic root namespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polecat:&lt;/strong&gt; Modular namespace design:&lt;/li&gt;
&lt;li&gt;Async LINQ operators live in &lt;code&gt;using Polecat.Linq;&lt;/code&gt; (&lt;code&gt;ToListAsync&lt;/code&gt;, &lt;code&gt;FirstOrDefaultAsync&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Batching types live in &lt;code&gt;using Polecat.Batching;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enum serialization and DDL types live in &lt;code&gt;using Weasel.Core;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Method Signatures &amp;amp; &lt;code&gt;CancellationToken&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marten:&lt;/strong&gt; Legacy convention where &lt;code&gt;CancellationToken&lt;/code&gt; was frequently the first argument:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadManyAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polecat:&lt;/strong&gt; Modern BCL style placing cancellation tokens at the end with default values:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadManyAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;System.Text.Json&lt;/code&gt; Configuration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marten:&lt;/strong&gt; Configured via:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSystemTextJsonForSerialization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnumStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Casing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configure&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polecat:&lt;/strong&gt; Configured via:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureSerialization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnumStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;casing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Casing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CamelCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configure&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;h3&gt;
  
  
  Startup Seeding (&lt;code&gt;IInitialData&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Marten:&lt;/strong&gt; &lt;code&gt;marten.InitializeWith&amp;lt;MartenInitialData&amp;gt;()&lt;/code&gt; (&lt;code&gt;Marten.Schema.IInitialData&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polecat:&lt;/strong&gt; &lt;code&gt;opts.InitialData.Add(new PolecatInitialData())&lt;/code&gt; (&lt;code&gt;Polecat.IInitialData&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detail in Polecat:&lt;/strong&gt; &lt;code&gt;AddPolecat&lt;/code&gt; unconditionally registers &lt;code&gt;PolecatActivator&lt;/code&gt; (&lt;code&gt;IHostedService&lt;/code&gt;), running &lt;code&gt;InitialData&lt;/code&gt; seeders reliably on startup even without calling &lt;code&gt;ApplyAllDatabaseChangesOnStartup()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. What Was Our Own Code &amp;amp; Migration Debt?
&lt;/h2&gt;

&lt;p&gt;The migration spike acted as an invaluable mirror, surfacing hidden technical debt that Marten's permissive nature had masked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transition to &lt;code&gt;CreateAsyncScope()&lt;/code&gt;:&lt;/strong&gt; Hosted background services and startup bootstrapping paths were updated from synchronous &lt;code&gt;CreateScope()&lt;/code&gt; to &lt;code&gt;provider.CreateAsyncScope()&lt;/code&gt; to properly dispose asynchronous database sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncovered Hidden Synchronous LINQ Execution:&lt;/strong&gt; Because Polecat throws &lt;code&gt;System.NotSupportedException&lt;/code&gt; on sync enumeration, the spike immediately flagged legacy queries accidentally calling &lt;code&gt;.ToList()&lt;/code&gt; instead of &lt;code&gt;.ToListAsync()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closure Null-Checks Inside &lt;code&gt;Where&lt;/code&gt; Predicates:&lt;/strong&gt; Polecat translates &lt;code&gt;(key == null)&lt;/code&gt; into a SQL column comparison rather than evaluating it locally in C#:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Fails: Polecat tries translating (key == null) into a SQL column comparison&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;run&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;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrganizationId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✓ Fix: Branch in C# before dispatching the query&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&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="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrganizationId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;run&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Spike Verdict: Polecat is Production-Ready
&lt;/h2&gt;

&lt;p&gt;Polecat is a real step forward for .NET teams tied to SQL Server 2025: native binary JSON, JSON indexes, and the Critter Stack (documents, Wolverine, schema generation) on an engine many enterprises already run.&lt;/p&gt;

&lt;p&gt;In our production migration it delivered where it counts — indexing, async LINQ, and a straightforward Wolverine swap. If PostgreSQL is off the table, Polecat is the first credible document story on SQL Server without feeling like a hack.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>sqlserver</category>
      <category>architecture</category>
    </item>
    <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>
