Using RedisJSON with NReJSON

This blog explores use of  the RedisJSON datatype in NetCore applications. It provides sample C# code that you can modify and use in your own applications. In all the examples shown, we'll be using the .NET  client NReJSON  (courtesy, Tommy Hanks et al).

Prerequisites
The samples reference the following packages from nuget.org:

a)  StackExchange.Redis -Version 2.1.58
b)  NReJSON -Version 3.1.0
c)  Newtonsoft.Json -Version 12.0.3

The samples were developed and tested with the versions shown; I imagine they'll work with later versions.

Sample Data

For this exercise, we'll need to create a RedisJSON datatype.  We'll create a controller with a POCO object UserInfo that will provide the initial data for the app.  The controller is shown below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

//RejSon
using StackExchange.Redis;
using NReJSON;
using Newtonsoft.Json;
using System.Dynamic;

namespace RedisSample.Controllers
{
    public class Address
    {
        public string street { get; set; }
        public string city { get; set; }
        public string state { get; set; }
        public string zip { get; set; }
    }
    public class UserInfo
    {
        public string userId { get; set; }
        public string fullName { get; set; }
        public string email { get; set; }
        public int rank { get; set; }
        public Address address { get; set; }
    }
    [Route("api/[controller]")]
    public class RedisJsonSampleController : ControllerBase
    {
        UserInfo _userInfo;
        private readonly IConnectionMultiplexer _connectionMultiplexer;
        public RedisJsonSampleController(){
            UserInfo userInfo = new UserInfo()
            {
                userId = "AC-67292",
                fullName = "Bob James",
                email = "bobjames@sample.com",
                rank = 15,
                address = new Address()
                {
                    street = "678 Winona Street",
                    city = "New Medford",
                    state = "Delaware",
                    zip = "12345"
                }
            };

            _userInfo = userInfo;
            
            //Replace with the IP of your redis server
            string conn = "127.0.0.1:6379"; 
            _connectionMultiplexer = ConnectionMultiplexer.Connect(conn);
        }
    }
}
RedisJson Sample Controller

To keep the code simple and to the point, I am creating the data in the constructor and also hard-coding the IP address of the redis server. Included in the UserInfo is "rank".  This is just to demonstrate use of the INCR command in a later example.  Feel free to use DI and other methods to improve your implementation.

I am also using [HttpGet] for all the API endpoints, even if the method changes data,  – again this is to keep things simple.

At any point you can verify the initial (non-RedisJSON) data using this code:

        [HttpGet]
        [Route("/api/redisjsonsample/initial/person")]
        public IActionResult GetPersonData()
        {
            return Ok(_userInfo);
        }
Get Initial Data

The next section of code creates the RedisJSON record and verifies that it is correct.  Note: throughout this sample, I am using the format userprofile:useremail , in this case – "userprofile:bobjames@sample.com"  – as the key.

        //********Save UserInfo to Redis**********
        [HttpGet]
        [Route("/api/redisjsonsample/save/profile")]  //save initial data
        public async Task<IActionResult> SaveUserProfile()
        {
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();
            string json = JsonConvert.SerializeObject(_userInfo);
            //use try/catch to catch error
            OperationResult result = await db.JsonSetAsync(key, json);

            if (result.IsSuccess) { return Ok("SaveUserProfile Succeeded"); }

            return BadRequest("SaveUserProfile  Failed");
        }

 		//********Read UserInfo from Redis**********
        [HttpGet]
        [Route("/api/redisjsonsample/get/profile")]  
        public async Task<IActionResult> GetUserProfile()
        {
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();    
            string[] parms = { "." };
            RedisResult result = await db.JsonGetAsync(key, parms);
            if (result.IsNull) { return BadRequest("GetUserProfile Failed"); }
            string profile = (string)result;
            return Ok(profile);
        }
Create Record and Check it.

Changing Values
With the RedisJSON record created, we can try to change some properties.  The code below shows how to change the "state" and the "fullName"

        [HttpGet]
        [Route("/api/redisjsonsample/change/state_plus_fullname")]
        public async Task<IActionResult> ChangeSomeProps()
        {
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();
            
            //change state -- note: full path is needed .address.state 
            //use try/catch to catch error
            OperationResult result = await db.JsonSetAsync(key, JsonConvert.SerializeObject("New York"), ".address.state");
            string resultValue = result.RawResult.ToString();  //- - > OK
           
            //change fullName
            //use try/catch to catch error
            OperationResult res = await  db.JsonSetAsync(key, JsonConvert.SerializeObject("Bob T Jones"),".fullName");

            string fullNameResult = res.RawResult.ToString(); //- - > OK  

            return Ok(resultValue + "**" + fullNameResult ) ; 
        
        }
Change state and fullName

Incrementing Numeric Values
Below is a sample on how to increment a numeric value.  Here, we are incrementing the rank by 3.

       [HttpGet]
        [Route("/api/redisjsonsample/incr/rank")]
        public async Task<IActionResult> IncrementRank()
        {
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();

            //note the value is being incremented by 3 -- hardcoded
            //In real life you'll pass it in as a parameter
			//use try/catch to catch error
            RedisResult res = await db.JsonIncrementNumberAsync(key, ".rank", 3);           
            //returns 15 + 3   --> 18  (the first time it's used)
            return Ok((string)res);
        }
Incrementing Numeric Values

Note: in the sample above, you can use a negative number if you want to decrement the number.

NReJSON also has the JsonMultiplyNumberAsync method for multiplying a numeric value.  The code is similar to the above.

Getting Multiple Properties
Redis Records can be 512 megabytes in size.  If you have huge JSON records you may want to limit your queries to specific keys  – for the same reasons why you may not want to limit use of SELECT * in SQL queries – and instead use SELECT column1, column3... instead.

The example below illustrates how to select fullName, state and zip from the RedisJSON  record.

        [HttpGet]
        [Route("/api/redisjsonsample/mprops")]
        public async Task<IActionResult> GetMultipleProps()
        {          
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();
            string[] multipleKeys = { ".fullName", ".address.state" , ".address.zip" };
            RedisResult result = await db.JsonGetAsync(key, multipleKeys       

            if (result.IsNull)
            {
                return BadRequest("bad request");
            }
            return Ok( (string)result);
             //NOTE: returns {".fullName":"Bob T Jones",".address.state":"New York",".address.zip":"12345"}
        }
Getting Multiple Properties

Important Notes
a) In this example the record is returned with preceding periods (".") for the properties because they are actually redis key/value pairs.

    {".fullName":"Bob T Jones",".address.state":"New York",".address.zip":"12345"}

b) If you include a key (eg .middleInitial ) that does not exist, you will get a RedisServerException error and no records will be returned.

Adding a New Property
In the .address.zip key on the preceding samples  we had a zip code "12345 Supposing we have to add a new field, zip+4, without changing the original zip. The example below shows how to do that.  Note this requires just adding the value to a new key, eg,  .address.zipPlusFour as shown in the example below.

   		[HttpGet]
        [Route("/api/redisjsonsample/add/addziplusfour")]
        public async Task<IActionResult> AddNewProperty()
        { 
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();

            string zipPlusFour = "12345-6789";
            string jsonZipPlusFour = JsonConvert.SerializeObject(zipPlusFour);
            OperationResult opResult = await db.JsonSetAsync(key, jsonZipPlusFour, ".address.zipPlusFour");

            if (opResult.IsSuccess)
            {
                string result = opResult.RawResult.ToString();
                return Ok(result); // --> OK
            }
            return BadRequest("Error - cannot create new property");
        }
Adding a new Property

Adding Whole Objects to  RedisJSON Datatype
The preceding example shows how to add a new property to the JSON record.  We can use the same approach to add entire objects. Let's say we wanted to add a shipTo address to our current .address key.  In our C# code we will create a new Address object and add it as shown below.

 		[HttpGet]
        [Route("/api/redisjsonsample/add/secondaddress")]
        public async Task<IActionResult> AddJsonObject()
        {
            Address shipTo = new Address()
            {
                 street="4592 Vacation Drive",
                 city = "Vacation City",
                 state = "New Hampshire",
                 zip ="36448"
            };
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();

            string jsonshipTo = JsonConvert.SerializeObject(shipTo);
            //Note the new key
            OperationResult opResult = await db.JsonSetAsync(key, jsonshipTo, ".address.shipTo");

           if (opResult.IsSuccess)
            {
                string result = opResult.RawResult.ToString();
                return Ok(result); // --> OK
            }
            return BadRequest("Error - cannot add object");
         }
Adding Objects

With this code implemented, we can then access the original street using the key .address.street and the shipTo street as .address.shipTo.street.

Implementing New Features/Executing Direct Commands
As features are added to Redis, there may be commands that may not yet be implemented in your RedisJSON client.  For instance, as of the time of this article (8/16/2020), the JSON.STRAPPEND command had not been implemented in our redis client NReJSON.  Thanks to Marc Gravell we can use the StackExchange.Redis Execute/ExecuteAsync methods to implement this and other new or unimplemented commands.

Below is an example of how we can run the JSON.STRAPPEND command to add " Boulevard" to the key ".address.shipTo.street".

        [Route("/api/redisjsonsample/strappend/tostreet")]
        public async Task<IActionResult> AppendString()
        {
            IDatabase db = _connectionMultiplexer.GetDatabase();
            string key = "userprofile:" + _userInfo.email.ToLower();
            string jsonStrappend = JsonConvert.SerializeObject(" Boulevard");
            int result = await db.ExecuteAsync("JSON.STRAPPEND", new object[] {key, ".address.shipTo.street", jsonStrappend });  
            return Ok(result);
        }
Executing Direct Commands

Notes:
a) The JSON.STRAPPEND command returns an integer – the length of the updated string.
b) You are encouraged to wrap the Execute method in a try/catch because if the path is invalid, a RedisServerException will be thrown.