EF Core vs Marten and Polecat
EF Core vs Marten and Polecat
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. 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.
The Aggregate Became a Join
The symptoms show up before anyone argues about databases. Loading one order walks every relationship the screen might need. Include chains grow. AsSplitQuery appears because one query returned a cartesian product. The Fluent API in OnModelCreating holds the mapping the class no longer shows.
LoadOrderEfCore.cs
var order = await db.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Discounts)
.Include(o => o.ShippingAddress)
.Include(o => o.StatusHistory)
.AsSplitQuery()
.SingleAsync(o => o.Id == orderId);
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. ToJson 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.
Why the default stays? Three habits, and they reinforce each other. 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. 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 Include chain above and calls that design.
One Roundtrip, Same Engine
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 mt_doc_order. The body is JSONB, or JSON on SQL Server. There is no line-item table unless you create one.
LoadOrderDocument.cs
var order = await session.LoadAsync(orderId);
order.ApplyDiscount(coupon);
session.Store(order);
await session.SaveChangesAsync();
Store is the upsert. The rules for Insert, Update, 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 Marten Event Sourcing in Production.
| EF Core | Marten / Polecat |
|---|---|
| Model | Tables and JOINs |
| Database | Your Postgres or SQL Server |
| ACID across models | Yes. Relational only. |
| Aggregates | Includes and mapping |
| Natural and typed | |
| Schema friction | Schema changes need a migration. |
| Low |
"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 DateTime 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. 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.
Both models, one instance I do not throw EF Core away to adopt the document. One PostgreSQL or SQL Server instance holds both. On one side, Marten or Polecat: the aggregates that are loaded and saved whole, one roundtrip by id, mt_doc_order. 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. 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. 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.
When to Choose Which
If your data is inherently relational-orders with line items, reports requiring multi-table joins, or schemas already consumed by other services via SQL-these patterns shine. They leverage existing infrastructure, avoid duplicate storage, and keep the domain model intact through the mapping layer. EF Core provides a familiar, staffable experience that aligns with third normal form principles, making migrations straightforward and reducing friction during evolution.
Conversely, if your primary concern is loading a single entity as a cohesive unit and minimizing roundtrips, the document approach offers compelling advantages. Marten on PostgreSQL and Polecat on SQL Server store aggregates as JSONB, enabling fast retrieval of complete objects without expensive joins. This is particularly valuable when the access pattern revolves around loading one order to make decisions-a scenario where the overhead of traversing relationships becomes prohibitive.
The critical question is not whether you prefer documents over tables, but whether your application's dominant operation matches either paradigm. A screen that wants five joins is signaling that the data is relational and should remain modeled as such. A command that loads one order to decide signals that the data is a document and deserves its own storage strategy. Neither choice requires abandoning the other entirely; many teams successfully operate both models within a single PostgreSQL or SQL Server instance, keeping strictly relational workloads on EF Core/Dapper while offloading document-centric workflows to Marten or Polecat.
Comments
No comments yet. Start the discussion.