
Marten has had its Patching API feature to do partial updates of document data in the database without first having to load documents since basically the beginning. We did that in the early days using JavaScript and the PLv8 extension for PostgreSQL, then starting in Marten 7.0, Babu built a version that works completely through native PostgreSQL JSON operators and PL/pgSQL functions without any extensions required.
The Patching API feature has been pretty stable for a long time, and most of our community focus is on the Event Sourcing side of things. After addressing a couple reported issues in our 9.30 release, it was nice to remember that Marten also has a very rich feature set as a document database.
Patching API
There is a workflow that every document database user has written a hundred times:
var order = await session.LoadAsync<Order>(orderId);
order.Status = OrderStatus.Shipped;
session.Store(order);
await session.SaveChangesAsync();Four lines to change one string. And underneath those four lines: a round trip to the database, a full JSON deserialization into an Order object, a mutation of one property, a full re-serialization of that object back to JSON, and a second round trip to write it. If Order is a fat document with a few hundred line items, you moved all of it across the wire twice to change six characters.
Worse, that read-modify-write is a race unless you've opted into optimistic concurrency. And if you need to make the same change to ten thousand documents, you're now writing a paging loop.
The Patching API is the alternative. It expresses the change rather than the result, sends only that change to the database, and lets the database apply it in place with its native JSON functions. The whole thing collapses to:
session.Patch<Order>(orderId).Set(x => x.Status, OrderStatus.Shipped);
await session.SaveChangesAsync();One statement, one round trip, no deserialization on either end.
One API, three databases
The code above is the same code whether your store is Marten on PostgreSQL, Polecat on SQL Server, or Fisher on SQLite. We're committed to maintaining and advancing all of our document database and event store libraries going forward. We've made that feasible by sharing much more common code and common compliance tests between the tools.
If you've written code against Marten's Patching API, you already know Polecat's and Fisher's.
Patch by id, or by criteria
Every patch starts one of two ways. By id:
theSession.Patch<Target>(target.Id).Set(x => x.Number, 10);
await theSession.SaveChangesAsync();Or by a Where expression, which applies the same change to every matching document in a single UPDATE:
// Change every Target document where the Color is Blue
theSession.Patch<Target>(x => x.Color == Colors.Blue).Set(x => x.Number, 2);That second form is the one that turns a migration script into a one-liner. There's no paging loop, no batch size to tune, and no client-side deserialization of ten thousand documents you were only going to touch one field on.
In Marten and Polecat, Patch<T>() is an extension method on IDocumentOperations. In Fisher it's a method directly on IDocumentSession. Either way you call it the same.
In every case, the property being changed can be a deep accessor -- x => x.Inner.Color reaches into a child object, and parents that don't exist yet get initialized on the way down.
Patching and Transactions
Just to make this clear, calling any Patch command is queueing up work in the current IDocumentSession unit of work. No actual database work will happen until IDocumentSession.SaveChangesAsync() is called. This allows you to batch up any number of Marten write operations and commit them in one single transaction. In most cases, Marten won't even be holding onto an open database connection until it has to. I call this behavior out because it is a little different than other persistence tools like EF Core where batch operations aren't part of their unit of work mechanics.
The operations
Set
The bread-and-butter operation, either by expression or by the raw stored key:
session.Patch<Order>(orderId)
.Set(x => x.Status, "Completed")
.Set(x => x.ShippedDate, DateTimeOffset.UtcNow);session.Patch<Order>(orderId)
.Set(x => x.Address.City, "New York");The string overload is how you introduce a property to documents that were persisted before that property existed:
const string where = "(data ->> 'UpdatedAt') is null";
theSession.Patch<Target>(new WhereFragment(where)).Set("UpdatedAt", DateTime.UtcNow);
await theSession.SaveChangesAsync();A note on those string overloads: they take the stored key, exactly as it appears in the JSON -- "name", not "Name", if you're on a camelCase naming policy. That's the entire point of them. They're for reaching keys your .NET type no longer has a member for, so they deliberately don't resolve through the member/naming machinery, which would refuse the very case they exist to handle.
Increment
theSession.Patch<Target>(target.Id).Increment(x => x.Number);Defaults to adding 1. Pass your own, including into a dictionary entry:
theSession.Patch<Target>(target.Id).Increment(x => x.NumberByKey["whatever"], 3);Decrementing is just a negative increment:
session.Patch<Order>(orderId).Increment(x => x.ItemCount, -1);This is the operation that most clearly justifies the whole API. An increment done through load-mutate-save is a read-modify-write race between two concurrent callers; an increment done as a patch is a single UPDATE statement that the database serializes for you.
Append, Insert, and Remove on child collections
theSession.Patch<Target>(target.Id).Append(x => x.Children, child);AppendIfNotExists() treats the collection as a set instead of a list and only appends when the element isn't already there:
session.Patch<Order>(orderId).AppendIfNotExists(x => x.Tags, "priority");Insert() puts an element at a specific index, appending when you don't give one:
session.Patch<Order>(orderId).Insert(x => x.Tags, "urgent", index: 0);Remove() takes the element out. By default it removes the first match; pass RemoveAction.RemoveAll to strip every occurrence:
theSession.Patch<Target>(target.Id).Remove(x => x.NumberArray, child);
theSession.Patch<Target>(target.Id).Remove(x => x.NumberArray, child, RemoveAction.RemoveAll);Complex elements work too -- matching is performed across all fields of the element:
theSession.Patch<Target>(target.Id).Remove(x => x.Children, child);Duplicate
Copy a value from one location to one or more others. The destinations don't have to exist yet:
theSession.Patch<Target>(target.Id).Duplicate(t => t.String, t => t.AnotherString);theSession.Patch<Target>(target.Id).Duplicate(t => t.String,
t => t.StringField,
t => t.Inner.String,
t => t.Inner.AnotherString);Rename
You renamed a property on your document type and you have a million rows persisted under the old name. This is the fix:
theSession.Patch<Target>(target.Id).Rename("String", x => x.Inner.AnotherString);Combine it with a Where clause and a property rename becomes a single statement against your entire table rather than a data migration project.
Delete
Drop a property you no longer want stored -- by name for a property your class no longer has, by lambda for one it still does:
theSession.Patch<Target>(target.Id).Delete("String");
theSession.Patch<Target>(target.Id).Delete("String", t => t.Inner);
theSession.Patch<Target>(target.Id).Delete(t => t.Inner);And across the whole table:
const string where = "(data ->> 'String') is not null";
theSession.Patch<Target>(new WhereFragment(where)).Delete("String");
await theSession.SaveChangesAsync();Chaining
Every operation returns the patch expression, so a chain composes into a single database statement:
theSession.Patch<Target>(target.Id)
.Set(x => x.Number, 10)
.Increment(x => x.Number, 10);
await theSession.SaveChangesAsync();That document ends up with Number == 20. Note that the operations are applied in order and each step sees the accumulated result of the ones before it -- a chain reads its own work.
This chaining is worth calling out because it's a genuine improvement over what came before. Marten's original PLv8-based patching could only do a single patch operation per database call. The native implementations do the whole chain in one statement.
What each database actually does
Same API, three very different translations underneath.
Marten / PostgreSQL. Marten's native patching -- which replaced the old Marten.PLv8 plugin as of Marten 7 -- is implemented as a PL/pgSQL function (mt_jsonb_patch) that Marten installs into your schema, driving Postgres' JSON operators. It's the most complete of the three implementations, and it's the one whose interface the others adopted.
Polecat / SQL Server. Each operation becomes an UPDATE built on SQL Server's JSON_MODIFY():
UPDATE pc_doc_order
SET data = JSON_MODIFY(data, '$.status', 'Shipped')
WHERE id = @idCollection operations lean on OPENJSON and STRING_AGG to take the array apart and put it back together.
Fisher / SQLite. This one is, honestly, the strongest single argument for the SQLite store. SQLite's json1 extension is built in -- there's no server-side function to install the way Marten needs one -- and every operation is a single json1 function inside one update … set data = …, so a chain nests into one statement with no gymnastics.
Fisher gets an extra dividend from an unrelated design decision: its duplicated fields are VIRTUAL generated columns computed over data, which means a duplicated field follows a patch with nothing to refresh. Marten and Polecat both have to update their duplicated columns inside the patch SQL. That's the clearest payoff of Fisher's generated-column approach.
The honest caveats
I'd rather you hear these from me than discover them in production.
A patch is not free. It avoids the deserialize/mutate/serialize round trip. It does not avoid the row rewrite. The JSON mutation functions re-render the document, so a patched row is no longer byte-identical to what your serializer would have written, and a newly added or renamed key tends to land at the end of the object. Don't let "patching avoids the round trip" quietly become "patching is cheap."
Timestamps are treacherous. Because the API depends on comparisons against the JSON as serialized in the database, DateTime and DateTimeOffset values frequently miss on comparison because of precision differences. Be careful using them as match values in Remove() or the IfNotExists variants.
The API is shared; the capabilities aren't perfectly identical. Marten's interface was adopted as the canonical superset, and there are corners the other two haven't reached yet. The predicate-based AppendIfNotExists, InsertIfNotExists, and Remove overloads throw NotSupportedException on Polecat today -- the predicate would need to be translated into a JSON array search, and that work hasn't been done. Use the element-based overloads there. Fisher, in the other direction, adds nullable Increment overloads that the other two don't carry. Check the docs for your store rather than assuming perfect parity.
Patches and soft deletes. A patch won't reach a soft-deleted row. That's the same rule the load SQL and the default LINQ filter follow, so it should be unsurprising, but it's worth stating.
Patches and projections. A patch changes a document directly. If that document is an event projection's snapshot, the next rebuild will overwrite whatever you patched -- a projection's rows are derived data, and a patch is not an event. If you want a durable change to a projected document, the change belongs in the event stream.
When to reach for it
Patching earns its place in a few clear situations:
- Bulk data migrations. Renaming a property, backfilling a new one, dropping a dead one. A
Where-clause patch does in one statement what would otherwise be a paged read-modify-write loop. - Counters and accumulators.
Increment()sidesteps the read-modify-write race entirely. - Large documents with small changes. The bigger the document relative to the edit, the more the round trip costs you and the more the patch saves.
- Set-style collection maintenance. Adding a tag, removing a flag, appending to an audit list.
And it's the wrong tool when the change depends on business logic that needs the loaded document to decide, when you're mutating an event-sourced aggregate (append an event instead), or when the document is small and the clarity of load-mutate-save is worth more than the round trip.
Go read the docs
Each store's reference page has the full operation list, the store-specific details, and runnable samples pulled straight from the test suites:
- Marten: Partial updates / patching
- Polecat: Partial updates / patching
- Fisher: Partial updates / patching
If you're on the old Marten.PLv8 patching plugin, the migration is about as easy as these things get: change using Marten.PLv8.Patching; to using Marten.Patching; and your existing code compiles and works. The API was deliberately kept identical. That plugin is deprecated, so it's worth doing now.



