Search code examples
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:

  • ServiceStack stores complex types in DB as blobbed JSV by default (last paragraph of first section: https://github.com/ServiceStack/ServiceStack.OrmLite), so it is necessary to set it the way it is proposed: MySqlDialect.Provider.StringSerializer = new JsonStringSerializer(); (https://github.com/ServiceStack/ServiceStack.OrmLite#pluggable-complex-type-serializers)
    => default JSV changed to JSON.
  • the ServiceStack's serialization does not work with structs, it is necessary to treat them special way:
    1. a) according to https://github.com/ServiceStack/ServiceStack.Text#c-structs-and-value-types and example https://github.com/ServiceStack/ServiceStack.Text/#using-structs-to-customize-json it is necessary to implement TStruct.ToString() and static TStruct.ParseJson()/ParseJsv() methods.

      b) according to https://github.com/ServiceStack/ServiceStack.Text/#typeserializer-details-jsv-format and unit tests https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/CustomStructTests.cs it shall be TStruct.ToString() (the same as in a) and static TStruct.Parse().

      Subquestion #1: which one is the right one? For me, ParseJson() was never called, Parse() was. Documentation issue or is it used in other situation?

      I implemented option b). Results:

      • IDbConnection.Save(_mainObject) saved the item to MariaDB. Success.
        Through the saving process ToString() and Parse() were called. In Parse, incoming JSON looked this way:
        "{\"StringProp\":\"SomeStruct's String\"}". Fine.
      • Serialization worked. Success.
      • Deserialization failed. I don't know the reason, but JSON incoming to Parse() was "double-escaped":
        "{\\\"StringProp\\\":\\\"SomeStruct's String\\\"}"

      Subquestion #2: Why the "double-escaping" in Parse on deserialization?

    2. I tried to solve structs with JsConfig (and Newtonsoft.Json to get proper JSON):

      JsConfig<SomeStruct>.SerializeFn = someStruct => JsonConvert.SerializeObject(someStruct);
      JsConfig<SomeStruct>.DeSerializeFn = json => JsonConvert.DeserializeObject<SomeStruct>(json);
      

      a) at first without ToString() and Parse() defined in the TStruct. Results:

      • Save failed: the json input in JsonConvert.DeserializeObject(json) that is used during Save was just type name "WinAmbPrototype.SomeStruct".
      • De/serialization worked.

      b) then I implemented ToString() also using Newtonsoft.Json. During Save ToString() was used instead of JsConfig.SerializeFn even the JsConfig.SerializeFn was still set (maybe by design, I do not judge). Results:

      • Save failed: but the json input of DeserializeFn called during Save changed, now it was JSV-like "{StringProp:SomeStruct's String}", but still not deserializable as JSON.
      • De/serialization worked.
    3. Then (during writing this I was still without any solution) I found JsConfig.Raw* "overrides" and tried them:

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

      a) at first without ToString() and Parse() defined in the TStruct. Results are the same as in 2a.

      b) then I implemented ToString(). Results:

      • BOTH WORKED. No Parse() method needed for this task.

      But it is very fragile setup:

      • if I removed ToString(), it failed (now I understand why, default ToString produced JSON with just type name in 2a, 3a).
      • if I removed RawSerializeFn setting, it failed in RawDeserializeFn ("double-escaped" JSON).

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):

  • if I am the TStruct owner: with just pure TStruct.ToString() and static TStruct.Parse() to support out of the box de/serialization and DB by ServiceStack (without different input in Parse()).
  • if I am a consumer of TStruct with no JSON support implemented and I am without access to its code: until now I did not find the way, if the ToString is not implemented: Save to DB did not work. Maybe would be fine to ensure JsConfig serialize functions are enough for both de/serialization and when used during saving to DB.

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();