Marten native partial updates - patching

Photo by Ryno Marais on Unsplash

Marten native partial updates - patching


5 min read

Partial update or patching JSON involves making changes to specific parts of a JSON document without replacing the entire document. This is particularly useful when you only need to modify certain fields or elements within a large JSON object, rather than rewriting the entire structure.

Marten supported patching by building an API on top of Postgres Extension PLV8 . Even though it worked well, there were few challenges all along as outlined below:

  • Not all cloud providers had this extension available for hosted Postgres instances. In several cases, we had to raise a request, based on up votes, cloud providers took their own time to make it available.

  • PLV8 is powered by V8 JavaScript engine. V8 changed their build mechanism to not provide pre-built binaries for each of the OSes. So this forced PLV8 maintainers to not provide pre-build PLV8 extensions which can be readily used since it will need a significant build infrastructure to target every OS flavor combination. This all lead to a scenario where PLV8 required end users to build it on their own from the source code in GitHub. Bear in mind that, just the V8 build takes easily an hour to do and it was a time consuming process. To run unit tests pertaining to Marten, we resorted to building custom docker images of the required Postgres versions with PLV8.

  • When you try to patch a long value using PLV8, it was truncating the value. See issue, this was eventually fixed in PLV8 v3.x with V8 adding support for BigInt. Albeit, availability and use of newer PLV8 was still an issue.

Due to the above issues, we moved the whole Marten PLV8 based patch library portion out of the core library and provided it as a separate opt-in plugin Marten.PLv8.

With above as context, we had created an issue back in May 2021 to support native partial updates or patching using Postgres PL/pgSQL and JSON operators. Finally, we turned around in January this year to support this via PR and the functionality is available in Marten v7.x as part of the core library.

The following are the supported operations:

  • Set the value of a persisted field or property

  • Add a new field or property with value

  • Duplicate a field or property to one or more destinations

  • Increment a numeric value by some increment (1 by default)

  • Append an element to a child array, list, or collection at the end

  • Insert an element into a child array, list, or collection at a given position

  • Remove an element from a child array, list, or collection

  • Rename a persisted field or property to a new name for structural document changes

  • Delete a persisted field or property

  • Patching multiple fields with the combination of the above operations. Earlier PLV8 API was able to do only one patch operation per DB call. With introduction of this new fluent API, this makes multi-field patching quite performant.

Also we maintain full compatibility with the Marten PLV8 patch API operations so that existing users can switch to use the native patching with very minimal changes i.e. change using Marten.PLv8.Patching; to using Marten.Patching;

Let us walk through each of the supported operations in a bit of detail including code snippets.

Set value of field or property

In the example below, we are running a series of patch operation using the fluent API as below:

  • Disable all users with role Author

  • Add a new field UpdatedAt with the current UTC date time.

  • Set nested property Location.City to null

session.Patch<User>(x => x.Role == UserRole.Author)  
    .Set(x => x.IsEnabled, false)  
    .Set("UpdateAt", DateTime.UtcNow)  
    .Set(x => x.Location, new Location() { City=null, Country=null});

Duplicate field to one or more new fields

In this example below, we are creating a duplicate of Location.Country to an immediate property named Country. You can see that the nesting is flattened.

session.Patch<User>(x => true)  
    .Duplicate(x => x.Location.Country, x => x.Country);

Also a field can be duplicated to one or more target fields.

Increment or decrement a number

In the example below, we are incrementing the failed login attempts by incrementing the value by 1.

session.Patch<User>(userId).Increment(x => x.LoginAttempts);

Similarly, you can use .Decrement(...) to decrease a value. By default the increment/decrement value is 1 and you can also pass a custom number value.

Append to an array field

In this example below, we are adding 2 new tag values to Tags

    .Append(x => x.Tags, "patch")
    .Append(x => x.Tags, "marten");

AppendIfNotExists(...) can be used to create an array while appending a value if it does not exist.

Values also can be appended at a specific position within an array by passing the position like Append(x => x.Tags, "patch", 1);. In this example, we are appending a value at the first location.

Remove a value from an array field

Session.Patch<BlogPost>(blogPostId).Remove(x => x.Tags, ".net5");

If the array contains multiple duplicate values then pass RemoveAction.RemoveAll to remove all of them.

Rename a field

In this example below, we are copying the value from property Location to Address and deleting Location in the process.

    .Rename("Location", x => x.Address);

Delete a field or property

In this example below, we are deleting a field named Location from all the records.

Session.Patch<User>(x => true).Delete("Location");

All the native patch operations are powered by a custom PL/pgSQL functions with prefix mt_jsonb_ have been implemented as part of the native patching functionality. You can readily use these as a building block to write your own patching system.

Please stay tuned, I will cover these functions in detail in the next blog post.