1

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.

martin
  • 125
  • 1
  • 7

1 Answers1

0

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();
mythz
  • 141,670
  • 29
  • 246
  • 390
  • Confirm, please, if I understood it correctly: you suggest TStruct.ToString()/.Parse() are enough for ServiceStack to make .ToJson and mainly .FromJson work? That's not what I've experienced. I've described it in point 2b where no JsConfig settings were applied: on pure deserialization using .FromJson in Test_Serialization call Parse gets "double-escaped" input string and fails. But when is Parse used as part of Save and SingleById, it gets correct input json. Please, try the example (without JsConfig) and check the input in Parse during .FromJson and compare when part of Save/SingleById. Thx. – martin Apr 03 '21 at 08:10
  • @martin only for **single scalar values** like a number or a string, if you want to serialize properties you need to use a class or a different serializer. – mythz Apr 03 '21 at 08:45
  • I'm sorry, I'm completely lost now. In short: I have TStruct.ToString which gives a string in some format. I have Parse that deserializes a string if it is in the right format. On Save the Parse is called with input in correct format but on .FromJson it is double-escaped. Why? What in my setup is different from ServiceStack's CustomStructTests where there are more scalar values in the struct like in mine? Why in the tests there is missing deserialization test? I don't expect ServiceStack to serialize my struct by itself, I just expect I get in Parse the same string ToString produced. – martin Apr 03 '21 at 10:49
  • @martin I don’t see any single scalar value in your example which shows you’re trying to serialize struct as DTO properties which isn’t supported. Use a class or different serializer. – mythz Apr 03 '21 at 10:56
  • I'm very sorry, I surely cannot understand the right meaning. But, please, what are those ServiceStack's tests about (here https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/CustomStructTests.cs and here https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/StructTests.cs)? Aren't they about de/serialization of struct providing ToString and Parse? – martin Apr 03 '21 at 11:09
  • @martin The `UserStat` Struct in your first link uses `ToString()` and `Parse()` which I've just tested works as expected in OrmLite. Last time I'm going to repeat this that unless your struct serializes into a [single scalar value](https://www.sqlserver-dba.com/2014/10/definition-of-a-scalar-value-in-a-relational-database.html), i.e. a number or a string, don't use ServiceStack's serializers. Your question is only showing trying to serialize a JSON Object which is the direct opposite of a scalar value and is specifically not supported for structs, only classes & what should be used instead. – mythz Apr 04 '21 at 13:44
  • I appreciate all your prompt answers and do not want to waste your precious time for this issue. Therefore I mark this as closed. But if you can answer, I have additional questions or notes: 1. `UserStat` may work as expected in OrmLite, my `SomeStruct` works in OrmLite too. That's the point I base on, because while working with OrmLite (having set JsonStringSerializer() as StringSerializer) the `Parse()` obtains correctly escaped string. I'm talking about `Parse()` during `FromJson()`, that is not tested with `UserStat` and `UserStat` does not have any string property that might be escaped. – martin Apr 08 '21 at 21:05
  • 2. I understand your docs the way: "we at ServiceStack do not process struct's properties ourselves, we ignore them. If you want them to be processed, implement 'the methods'. If your `ToString()` returns a string in some format, your `Parse()` shall be able to parse that format." Am I wrong? My `ToString()` returns a string in some format and `Parse()` is able to parse it, but the content of the Parse's input comes different from ToString's output (at least during `FromJson()` call, because during `Save()` it works). Does that mean the format of the ToString's output shall not be JSON string? – martin Apr 08 '21 at 21:05
  • 3. I serialize my `SomeStruct` to a string (= single scalar value) having JSON format (not object) inside. You use ':' to separate `UserStat` struct's properties in serialized output, I used JSON string. Is this the main problem? OrmLite can work with it only `FromJson()` cannot. Little off topic: in `UserStat` test there is small error in `ToString()` that does not show in test, because the test is not correct: it uses the same number for all `int` properties and therefore does not point to the `ToString()` error. Just to let you know. Thanks much for your help and time. – martin Apr 08 '21 at 21:06