1

A StockOverflowException is being caused by the code below, inside the WriteJson method at the call to JObject.FromObject(value). It recalls the WriteJson method.

How can I rewrite the AggregateEventConverter to avoid the recursive stack overflow problem?

And because I know somebody's going to ask, the code is written this way because events are written permanently to a stream and need to be able to be deserialized accurately years later after other coders have refactored the names of old event classes. For example, they may change class AppleFellOffTree to class AppleFellOffTree_v001, deprecating it but keeping it in the assembly for the purpose of deserializing old events. The AggregateEventTypeId attribute helps deserialize json into the correct classes, so long as coders keep those attributes intact while shifting/refactoring the event classes.

Newtonsoft's own TypeNameHandling feature doesn't help accurately deserialize classes whose names have been refactored.

class Program {
    static void Main(string[] args) {
        var e1 = new AppleFellOffTree {
            At = TimeStamp.Now,
            Id = Guid.NewGuid(),
            VersionNumber = 21,
        };
        var json = JsonConvert.SerializeObject(e1);
        var e2 = JsonConvert.DeserializeObject<AggregateEvent>(json);
    }
}

[Serializable]
[JsonConverter(typeof(AggregateEventConverter))]
public class AggregateEvent {
    public string EventName => GetType().Name;
    public Guid Id;
    public int VersionNumber;
    public TimeStamp At;
}

[AggregateEventTypeId("{44B9114E-085F-4D19-A142-0AC76573602B}")]
public class AppleFellOffTree : AggregateEvent {
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class AggregateEventTypeIdAttribute : Attribute {
    public readonly Guid Id;
    public AggregateEventTypeIdAttribute(string guid) {
        Id = Guid.Parse(guid);
    }
}

public class AggregateEventConverter : JsonConverter {

    public override bool CanRead => true;
    public override bool CanWrite => true;
    public override bool CanConvert(Type objectType) => objectType == typeof(AggregateEvent) || objectType.IsSubclassOf(typeof(AggregateEvent));

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
        if (null == value) {
            writer.WriteValue(value);
            return;
        }
        var jObject = JObject.FromObject(value);
        jObject.Add("$typeId", EventTypes.GetEventTypeId(value.GetType()));
        jObject.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
        var jToken = JToken.ReadFrom(reader);
        if (jToken.Type != JTokenType.Object) {
            throw new NotImplementedException();
        } else {
            var jObject = (JObject)jToken;
            var eventTypeId = (Guid)jObject.GetValue("$typeId");
            var eventType = EventTypes.GetEventType(eventTypeId);
            return JsonConvert.DeserializeObject(jToken.ToString(), eventType);
        }
    }
}

internal static class EventTypes {

    static readonly Dictionary<Guid, Type> Data = new Dictionary<Guid, Type>();

    static EventTypes() {

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        var eventTypes = assemblies.SelectMany(a => a.GetTypes()
            .Where(t => t.IsSubclassOf(typeof(AggregateEvent)))
            .Where(t => !t.IsAbstract))
            .ToArray();

        // t is for eventType
        foreach (var t in eventTypes) {
            var id = GetEventTypeId(t);
            if (Data.ContainsKey(id))
                throw new Exception($"Duplicate {nameof(AggregateEventTypeIdAttribute)} value found on types '{t.FullName}' and '{Data[id].FullName}'");
            Data[id] = t;
        }
    }

    public static Type GetEventType(Guid eventTypeId) {
        return Data[eventTypeId];
    }

    public static Guid GetEventTypeId(Type type) {

        // a is for attribute
        var a = type.GetCustomAttributes(typeof(AggregateEventTypeIdAttribute), false)
            .Cast<AggregateEventTypeIdAttribute>()
            .FirstOrDefault();

        if (null == a)
            throw new Exception($"{nameof(AggregateEventTypeIdAttribute)} attribute does not exist on type {type.FullName}.");

        if (Guid.Empty == a.Id)
            throw new Exception($"{nameof(AggregateEventTypeIdAttribute)} attribute was not set to a proper value on type {type.FullName}");

        return a.Id;
    }

    public static IEnumerable<KeyValuePair<Guid, Type>> GetAll => Data;
}
bboyle1234
  • 4,859
  • 2
  • 24
  • 29
  • 2
    Looks like a duplicate of [How to call JsonConvert.DeserializeObject and disable a JsonConverter applied to a base type via `[JsonConverter]`?](https://stackoverflow.com/q/45547123/3744182). The answers from [JSON.Net throws StackOverflowException when using `[JsonConvert()]`](https://stackoverflow.com/q/29719509/3744182) might also work. – dbc Dec 23 '17 at 16:38
  • If the linked solutions don't work for you, please let me know where the problem is and I'll re-open the question. – dbc Dec 23 '17 at 22:00
  • @dbc Those links helped, thank you. Please re-open the question so I can post the code I ended up using. – bboyle1234 Dec 24 '17 at 00:49
  • https://gist.github.com/bboyle1234/46291a8c8d42f797405057844eeb4bda – bboyle1234 Dec 24 '17 at 00:56

1 Answers1

1

After reading the links provided in the comments, I came up with this solution.

https://gist.github.com/bboyle1234/46291a8c8d42f797405057844eeb4bda

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

namespace Migratable {
    [JsonConverter(typeof(MigratableConverter))]
    public interface IMigratable {
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    public class MigratableAttribute : Attribute {
        public readonly Guid Id;
        public MigratableAttribute(string guid) {
            Id = Guid.Parse(guid);
        }
    }

    public class MigratableConverter : JsonConverter {

        [ThreadStatic]
        static bool writeDisabled = false;

        [ThreadStatic]
        static bool readDisabled = false;

        public override bool CanRead => !readDisabled;
        public override bool CanWrite => !writeDisabled;
        public override bool CanConvert(Type objectType) => typeof(IMigratable).IsAssignableFrom(objectType);

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
            try {
                writeDisabled = true;
                if (null == value) {
                    writer.WriteValue(value);
                } else {
                    var jObject = JObject.FromObject(value);
                    jObject.Add("$typeId", MigratableTypes.GetTypeId(value.GetType()));
                    jObject.WriteTo(writer);
                }
            } finally {
                writeDisabled = false;
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
            try {
                readDisabled = true;
                var jObject = JToken.ReadFrom(reader) as JObject;
                if (null == jObject) return null;
                var typeId = (Guid)jObject.GetValue("$typeId");
                var type = MigratableTypes.GetType(typeId);
                return JsonConvert.DeserializeObject(jObject.ToString(), type);
            } finally {
                readDisabled = false;
            }
        }
    }

    internal static class MigratableTypes {

        static readonly Dictionary<Guid, Type> Data = new Dictionary<Guid, Type>();

        static MigratableTypes() {
            foreach (var type in GetIMigratableTypes()) {
                CheckIMigratableRules(type);
                Data[GetTypeId(type)] = type;
            }
        }

        static IEnumerable<Type> GetIMigratableTypes() {
            return AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(a => a.GetTypes()
                .Where(t => typeof(IMigratable).IsAssignableFrom(t))
                .Where(t => !t.IsAbstract));
        }

        static void CheckIMigratableRules(Type type) {

            // Check for duplicate IMigratable identifiers
            var id = GetTypeId(type);
            if (Data.ContainsKey(id))
                throw new Exception($"Duplicate '{nameof(MigratableAttribute)}' value found on types '{type.FullName}' and '{Data[id].FullName}'.");

            // [DataContract] attribute is required, on EVERY class, not just base classes
            if (type.GetCustomAttributes(typeof(DataContractAttribute), false).Length == 0)
                throw new Exception($"'{nameof(IMigratable)}' objects are required to use the '[DataContract]' attribute. Class: '{type.FullName}'.");

            // Collect information about [DataMember] attributes on all fields and properties including inherited and private.
            var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            var fields = type.GetFields(bindingFlags).Where(f => null != f.GetCustomAttribute(typeof(DataMemberAttribute))).ToArray();
            var properties = type.GetProperties(bindingFlags).Where(p => null != p.GetCustomAttribute(typeof(DataMemberAttribute))).ToArray();
            var members = fields.Cast<MemberInfo>().Concat(properties.Cast<MemberInfo>())
                .Select(m => new {
                    Member = m,
                    DataMemberAttribute = (DataMemberAttribute)m.GetCustomAttribute(typeof(DataMemberAttribute))
                }).ToArray();

            // Check that DataMember names are explicitly set eg [DataMember(Name = "xx")]
            var noName = members.FirstOrDefault(m => !m.DataMemberAttribute.IsNameSetExplicitly);
            if (null != noName) {
                var message = $"'{nameof(IMigratable)}' objects are required to set DataMember names explicitly. Class: '{type.FullName}', Field: '{noName.Member.Name}'.";
                throw new Exception(message);
            }

            // Check that DataMember names are not accidentally duplicated.
            var duplicateName = members.GroupBy(m => m.DataMemberAttribute.Name).FirstOrDefault(g => g.Count() > 1);
            if (null != duplicateName) {
                throw new Exception($"Duplicate DataMemberName '{duplicateName.Key}' found on class '{type.FullName}'.");
            }
        }

        public static Type GetType(Guid typeId) {
            return Data[typeId];
        }

        public static Guid GetTypeId(Type type) {

            var a = type.GetCustomAttributes(typeof(MigratableAttribute), false)
                .Cast<MigratableAttribute>()
                .FirstOrDefault();

            if (null == a)
                throw new Exception($"'{nameof(MigratableAttribute)}' attribute does not exist on type '{type.FullName}'.");

            if (Guid.Empty == a.Id)
                throw new Exception($"'{nameof(MigratableAttribute)}' attribute was not set to a proper value on type '{type.FullName}'.");

            return a.Id;
        }
    }
}
bboyle1234
  • 4,859
  • 2
  • 24
  • 29