3

I have a type like this:

class Foo<T>
{
  public string Text { get; set; }

  public T Nested { get; set; }

  public static string ToJson(Foo<T> foo) { [...] }
}

ToJson serializes a Foo<Bar> instance to JSON in a way that is impossible to achieve by tweaking JsConfig. Also, ToJson relies on ServiceStack.Text to serialize Nested, which can be an instance of Foo<Baz>.

Unfortunately, the way JsConfig is implemented implies that there will be a JsConfig<T> set of static variables for Foo<Bar> and other for Foo<Baz>. Also, AFAIK, ServiceStack.Text offers no way to configure JSON serialization for open generic types (i.e.: something like JsConfig.Add(typeof(Foo<>), config)). I tried to solve this issue by creating this static constructor for Foo<T>:

static Foo() {
  JsConfig<Foo<T>>.RawSerializeFn = ToJson;
}

This doesn't work all the time. It depends on the order the static constructors are invoked by the runtime. Apparently, ServiceStack.Text caches serializers functions and sometimes is doing it before the static constructor is called depending on the order operations are invoked in the API, so:

var outer = new Foo<Baz> { Text = "text" };
outer.ToJson(); // OK, because Nested is null

var inner = new Foo<Bar>();
inner.ToJson(); // OK, because JsConfig<Foo<Bar>>.RawSerializeFn is Foo<T>.ToJson

outer.Nested = inner;
outer.ToJson(); // NOT OK, because SS.Text uses the default serializer for Foo<T>, not Foo<T>.ToJson

I can't set all the serializers in JsConfig<Foo<T>> beforehand because T can be virtually any type, even other generic types.

Is it possible to define custom serialization routines for open generic types (that can be nested) in ServiceStack.Text?

ygormutti
  • 358
  • 3
  • 17
  • Is ServiceStack's G+ group or GitHub issues a better place to ask this question than StackOverflow? Please let me know if you think so. – ygormutti May 16 '14 at 23:35

1 Answers1

2

I solved for this in my own way with a wrapper and a custom deserializer. I created a base type for all of my abstract types. That base type tells the system which type it is:

public class SetSettingItemBase
{
    public string Key { get; set; }
    public string ValueType { get; set; }
}

So the base is essentially the metadata -- the setting key + the value type. The object DTO, then, simply extends it by adding the actual value:

public class SetSettingItem : SetSettingItemBase
{
    public object Value { get; set; }
}

Note that it's just an object. This is the DTO, not my actual object. I can cast it later, or convert it to a real/generic type after serialization.

My custom serialization then is:

JsConfig<SetSettingItem>.RawDeserializeFn = str =>
            {
                var baseSettings = str.FromJson<SetSettingItemBase>();
                var ret = baseSettings.MapTo<SetSettingItem>();

                if(true) // actual condition removed here... unimportant to the example
                {
                    var dataType = Constants.KnownSettingsTypes[baseSettings.ValueType];

                    var method = typeof(JsonExtensions).GetMethod("JsonTo").MakeGenericMethod(dataType);

                    var key = "Value";
                    var parsed = JsonObject.Parse(str);

                    if(parsed.Object(key) == null)
                        key = "value";

                    ret.Value = method.Invoke(null, new object[] { parsed, key });
                }
                return ret;
            };

This method first deserializes to the simple base. So the Value passed in from the DTO is ignored when deserializing baseSettings. I then call MapTo to prepare the actual SetSettingItem DTO. MapTo is just a wrapper around AutoMapper. You could just as easily use SS's built in mapper here.

For security, I have a set list of types that I allow as settings. Example:

KnownSettingsTypes.Add("string", typeof(string));
KnownSettingsTypes.Add("int", typeof(int));
KnownSettingsTypes.Add("nullableint", typeof(int?));
KnownSettingsTypes.Add("nullablepercentage", typeof(double?));
KnownSettingsTypes.Add("feegrid", typeof(FeeGrid));

After that, I use reflection to get the JsonTo method, passing in the generic type parameter dynamically from the KnownSettingsTypes dictionary.

And then finishing it all up, I parse the object using the generic JsonObject.Parse method and then looking for the Value or value (depending on case sensitivity) and explicitly convert that JsonObject using the dynamic method I created earlier.

The end result is I can pass in settings of all different types here as a part of my DTOs.

This served my purposes for the time being, but looking at the example I could see it improving in two ways:

  1. After parsing, I could convert my SetSettingItem to a SettingItem<T> so I could use it as a strongly-typed object in my code. Remember, this example is just for the DTOs to get it across the wire.
  2. Instead of requiring the person to pass in the type for me to check against, I could check against the setting Key to know which type it is supposed to be and parse accordingly. In my example, even if I check against the master list of settings and their types, I'd still probably require them to pass in the type just as a precaution and throw an exception if they didn't match.
Eli Gassert
  • 9,745
  • 3
  • 30
  • 39
  • Do you have to wrap every SettingItem with a SetSettingItem? Couldn't you use __type (`JsConfig.IncludeTypeInfo`) field to achieve the same result without the wrapper? – ygormutti May 22 '14 at 18:28
  • Yes but I wanted more control. I didn't want Type coming through as **[System.Nullable``1[[System.Int32]]]** or the likes. I wanted a nice, clean type. Also, as I stated in my answer, I will likely eventually take type out of the equation and look it up based on `Key`, making type obsolete. – Eli Gassert May 22 '14 at 18:50
  • I could wrap every `Foo` with a `Foo` in each DTO that has a `Foo` and set `JsConfig.RawSerializeFn`. That would probably work, but then I couldn't use the strong-typed members, which is the purpose of having a `Foo` instead of just `Foo`. – ygormutti May 22 '14 at 19:47
  • I would wrap it in a `FooDto` not a `Foo`. Once you have the DTO serialized/deserialized, you can have another process that converts it to a `Foo` for processing. The distinction that I'm trying to make here is that the method I described is for helping the serialization processes and **NOT** for working with data inside my app. If you're on board with that and this still doesn't help, then perhaps I'm not understanding what you want to do. – Eli Gassert May 22 '14 at 22:52
  • Think of `Foo` in my previous comment as a DTO. I think I understand your approach, but I can't see how "another process that converts it to a `Foo` for processing" can lead to code as strongly-typed and clean as `(ToJson|FromJson)>`. Most type checking would be postponed to runtime and there would be lots of conversions everywhere. Do you agree or am I missing something? – ygormutti May 23 '14 at 00:37
  • 1
    Please, don't get me wrong: I think this answer is very useful, but you're not serializing `Foo`, you're serializing a wrapper. An answer that doesn't involve wrapping every instance of the generic type would be far more elegant and is what I'm looking for. – ygormutti May 23 '14 at 00:40