jsonormlite-servicestackservicestack-textmariadb-10.5

How to use ServiceStack to store POCOs to MariaDB having complex types (objects and structs) blobbed as JSON?


I've got following setup: C#, ServiceStack, MariaDB, POCOs with objects and structs, JSON.

The main question is: how to use ServiceStack to store POCOs to MariaDB having complex types (objects and structs) blobbed as JSON and still have working de/serialization of the same POCOs? All of these single tasks are supported, but I had problems when all put together mainly because of structs.

... finally during writing this I found some solution and it may look like I answered my own question, but I still would like to know the answer from more skilled people, because the solution I found is a little bit complicated, I think. Details and two subquestions arise later in the context.

Sorry for the length and for possible misinformation caused by my limited knowledge.

Simple example. This is the final working one I ended with. At the beginning there were no SomeStruct.ToString()/Parse() methods and no JsConfig settings.

using Newtonsoft.Json;
using ServiceStack;
using ServiceStack.DataAnnotations;
using ServiceStack.OrmLite;
using ServiceStack.Text;
using System.Diagnostics;

namespace Test
{
    public class MainObject
    {
        public int Id { get; set; }
        public string StringProp { get; set; }
        public SomeObject ObjectProp { get; set; }
        public SomeStruct StructProp { get; set; }
    }

    public class SomeObject
    {
        public string StringProp { get; set; }
    }

    public struct SomeStruct
    {
        public string StringProp { get; set; }

        public override string ToString()
        {
            // Unable to use .ToJson() here (ServiceStack does not serialize structs).
            // Unable to use ServiceStack's JSON.stringify here because it just takes ToString() => stack overflow.
            // => Therefore Newtonsoft.Json used.
            var serializedStruct = JsonConvert.SerializeObject(this);
            return serializedStruct;
        }

        public static SomeStruct Parse(string json)
        {
            // This method behaves differently for just deserialization or when part of Save().
            // Details in the text.
            // After playing with different options of altering the json input I ended with just taking what comes.
            // After all it is not necessary, but maybe useful in other situations.
            var structItem = JsonConvert.DeserializeObject<SomeStruct>(json);
            return structItem;
        }
    }

    internal class ServiceStackMariaDbStructTest
    {
        private readonly MainObject _mainObject = new MainObject
        {
            ObjectProp = new SomeObject { StringProp = "SomeObject's String" },
            StringProp = "MainObject's String",
            StructProp = new SomeStruct { StringProp = "SomeStruct's String" }
        };

        public ServiceStackMariaDbStructTest()
        {
            // This one line is needed to store complex types as blobbed JSON in MariaDB.
            MySqlDialect.Provider.StringSerializer = new JsonStringSerializer();

            JsConfig<SomeStruct>.RawSerializeFn = someStruct => JsonConvert.SerializeObject(someStruct);
            JsConfig<SomeStruct>.RawDeserializeFn = json => JsonConvert.DeserializeObject<SomeStruct>(json);
        }

        public void Test_Serialization()
        {
            try
            {
                var json = _mainObject.ToJson();
                if (!string.IsNullOrEmpty(json))
                {
                    var objBack = json.FromJson<MainObject>();
                }
            }
            catch (System.Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

        public void Test_Save()
        {
            var cs = "ConnectionStringToMariaDB";
            var dbf = new OrmLiteConnectionFactory(cs, MySqlDialect.Provider);
            using var db = dbf.OpenDbConnection();
            db.DropAndCreateTable<MainObject>();

            try
            {
                db.Save(_mainObject);
                var dbObject = db.SingleById<MainObject>(_mainObject.Id);
            }
            catch (System.Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }
    }
}

What (I think) I know / have tried but at first didn't help to solve it myself:

Is there some simpler solution? I would be very glad if someone points me to better direction.

Acceptable would be maybe two (both of them accessible because of different circumstances):

And the best one would be without employing other dependency (e.g. Newtonsoft.Json) to serialize structs. Maybe some JsConfig.ShallProcessStructs = true; (WARNING: just a tip, not working as of 2021-04-02) would be fine for such situations.


Solution

  • ServiceStack treats structs like a single scalar value type, just like most of the core BCL Value Types (e.g. TimeSpan, DateTime, etc). Overloading the Parse() and ToString() methods and Struct's Constructor let you control the serialization/deserialization of custom structs.

    Docs have been corrected. Structs use Parse whilst classes use ParseJson/ParseJsv

    If you want to serialize a models properties I'd suggest you use a class instead as the behavior you're looking for is that of a POCO DTO.

    If you want to have structs serailized as DTOs in your RDBMS an alternative you can try is to just use JSON.NET for the complex type serialization, e.g:

    public class JsonNetStringSerializer : IStringSerializer
    {
        public To DeserializeFromString<To>(string serializedText) => 
            JsonConvert.DeserializeObject<To>(serializedText);
    
        public object DeserializeFromString(string serializedText, Type type) =>
            JsonConvert.DeserializeObject(serializedText, type);
    
        public string SerializeToString<TFrom>(TFrom from) => 
            JsonConvert.SerializeObject(from);
    }
    
    MySqlDialect.Provider.StringSerializer = new JsonNetStringSerializer();