This blog post is based on an earlier blog post by Jeff Morris that covered the sub-document API while it was still in developer preview. There have been some changes to the API since that release.

With Couchbase Server 4.5 and the .NET SDK 2.3.x, you can now use the Sub-document feature in your .NET application.

In previous Couchbase releases, all document mutations were atomic and involve the entire document. If you only want to change a single field and then do an update, the entire document in Couchbase server is copied over by the new revision. The problem with is that if the document is large or the network slow (or both), then a lot of resources are wasted sending data that hasn’t been modified. A better, more performant solution would be to just send the portion of the document or the value which has been mutated. Essentially, that is what you get with sub-document API; when you update an element or delete an element of a document, only the path of the fragment to be mutated is sent over the wire and only that part of the document is modified.

Subdocument

There are several different operations that are supported by the API, from mutations on individual nested elements (aka sub-documents) to array and dictionary modifications. Counter operations are also supported, as are retrieval operations for embedded JSON fragments.

The API is exposed via a fluent interface which allows you to append multiple operations and then execute them against the document atomically. There are two different “builders”: a builder for mutation operations and a builder for reads or “lookups” (which can also check if an element exists at a given path).

Prerequisite: Couchbase Server 4.5

In order to follow the examples below, you’ll need to download and install Couchbase Server 4.5. If you’ve never installed Couchbase Server before, you can check out my video on how to install Couchbase Server on Windows. It’s really easy, no matter what OS you are using.

Sub-Document API Overview

The following examples will use a document with an id of “puppy” and will start out looking like this:

All of the examples are available on Github so you can clone the project and play around with the API.

MutateInBuilder and LookupInBuilder

The Sub-document API offers two new Types that utilize a builder pattern via a fluent-interface for chaining together multiple operations on a document. Both objects are created by calling MutateIn or LookupIn on a CouchbaseBucket object and passing in the key of the document you are working against:

Once you have the builder object, you can chain together a number of operations to execute against the document, for example:

Then you can send all of the operations to the server in a single batch:

You can check the result of one operation, by using OpStatus and a path. In this example, I’m using the path "type":

These are some of the methods and fields you’ll find on the IDocumentFragment interface.

Name

Description

Content(…​)

Gets the content for a given path or index.

Exists(…​)

Returns true if there is a result for a given path or index.

Count()

The count of current operations maintained by the builder.

OpStatus(…​)

The ResponseStatus of an operation at a given index or path.

Status

The ResponseStatus for the entire multi-operation.

Success

True if the entire multi-operation succeeds.

Besides these properties or methods, there are all of the other properties inherited from OperationResult (which is the standard response from a key/value operation): Upsert, Remove, Replace, and so on.

Error Handling

When sending multiple mutations, if one of them fails, the entire multi-operation request fails. This allows transactional “all-or-nothing” semantics when performing mutations within a single document.

When sending multiple lookups, some operations may succeed and some may fail, with the server attempting to return as many items as requested.

If the operation(s) failed, then theStatus property will contain a top-level error response such as SubDocMultiPathFailure. This is an indication that you should dig deeper into the operation’s results to get the specific error. You can do this by iterating: calling the OpStatus method and passing either the index or the path:

In this case, since the path “somepaththatdoesntexist” didn’t exist within the document, the specific error returned was SubDocPathNotFound. There are many different combinations of errors depending upon the builder type and the condition for the error.

LookupInBuilder Examples

The LookUpInBuilder type supports two operations: fetching a value by path and checking for the existence of a value at a given path.

Get:

Let’s lookup the owner fragment. If I pass in “owner” as the path parameter to this method…​

…​the output to console would be:

Exist:

We can also check to see if a path exists. If I pass in “owner” as the path to this method…​

…​the output is true, because the path owner does indeed exist within the document.

MutateInBuilder

The MutateInBuilder offers a number of methods supporting mutations on scalar values, dictionaries and arrays, along with support for atomic counter operations.

Insert:

Insert adds a value to a dictionary, optionally allowing for the containing element (the dictionary itself) to be added.

If I called the above method like so:

Then the document’s attributes dictionary will now look like this:

Note that the Insert method has an optional boolean parameter called createParents. It is false by default. If it’s true, then the sub-document API will create the necessary path for the field to exist. If it’s false, the sub-document API will only create the field if the parents of the field already exist. In the above example, the attributes field already existed.

In this next example, I’ll use a path with a parent field (anewattribute) that doesn’t already exist.

This will create the new attribute called anewattribute in the document and add a single key called withakey with a value of somevalue.

Now, if we passed false for createParents and the parent attribute did not exist, then the multi-mutation would fail with a top-level response status of SubDocMultiPathFailure and the specific error would be SubDocPathNotFound.

Upsert

Upsert will add or replace an existing dictionary entry. The usage is exactly the same as Insert with the exception of the method name being Upsert.

Remove

Remove will remove an element at a given path.

When I call this method:

Here’s how the document will look afterwards:

Replace

Replace will swap the value of element at a given path, failing if the path does not exist:

After I call this method:

The document will now have a different value for “owner”:

ArrayAppend

ArrayAppend adds a value to the end of an array, optionally adding the parent element (the array element itself) if it doesn’t exist.

After that method with the “toys” path…​

…​the toys array in the document will then have the value “slipper” in the last ordinal:

ArrayPrepend

ArrayPrepend works the same way as ArrayAppend, except it adds a value to the front of an array.

Calling that method with the “toys” path…​

The toys array now has the value “slipper” in it’s first ordinal:

ArrayInsert

ArrayPrepend puts a value at the beginning, ArrayAppend puts it at the end. Just to round things out, you can use ArrayInsert to put a value somewhere in between (at a given index).

And then calling that method with “toys[2]”…​

The toys array now has the value “slipper” at it’s 3rd ordinal (index 2):

ArrayAddUnique

ArrayAddUnique will inserts a value into an array, but it will fail if that value already exists (i.e. the value must be unique within the array).

When I call that with “shoe”…​

…​since the value “shoe” already exists in the original document’s toys array, this will fail with the status SubDocPathExists.

Note that this method only allows for JSON primitives to be inserted: strings, numbers, and special values for true, false or null. There is no way to compare for uniqueness without descending into each JSON object and comparing elements item by item.

Counter

Adds the specified delta (change) to an existing value, creating the element if it doesn’t exist. It will default both the value and delta to 0. If the delta is negative, the value of the element will be decremented by the given delta.

I’ll create a method that uses Counter:

Then, I’ll call the method twice using a positive 1 and a negative 1 as the “deltas”:

After the first call, since the element doesn’t exist it will be created and then set to one (1). The document will now look like this:

The second call passes a negative one (-1), so then the counter for likes will be decremented to zero (0). The JSON document will now look like this:

Conclusion

Since the developer preview blog post, the Couchbase .NET SDK has been updated to version 2.3.2 (as of the time of this blog post). You can check out the work that’s been done in the Release Notes of Version 2.3.2.

Final Notes

The sub-document API gives you the ability to be more granular in your interactions with documents. You can modify and retrieve just the portions that you need.

Leave a comment below, talk to me on Twitter, or email me (matthew.groves AT couchbase DOT com) if you have any questions or comments.

Author

Posted by Matthew Groves

Matthew D. Groves is a guy who loves to code. It doesn't matter if it's C#, jQuery, or PHP: he'll submit pull requests for anything. He has been coding professionally ever since he wrote a QuickBASIC point-of-sale app for his parent's pizza shop back in the 90s. He currently works as a Senior Product Marketing Manager for Couchbase. His free time is spent with his family, watching the Reds, and getting involved in the developer community. He is the author of AOP in .NET, Pro Microservices in .NET, a Pluralsight author, and a Microsoft MVP.

Leave a reply