使用JObject.FromValue时的StackOverflow

问题描述 投票:1回答:1

在调用WriteJson时,JObject.FromObject(value)方法内部的代码引起了StockOverflowException。它回忆起WriteJson方法。

如何重写AggregateEventConverter以避免递归堆栈溢出问题?

而且因为我知道有人会问,代码是这样编写的,因为事件是永久写入流的,并且需要能够在其他编码器重构旧事件类的名称之后几年后准确地反序列化。例如,他们可能会将class AppleFellOffTree更改为class AppleFellOffTree_v001,弃用它但将其保留在程序集中以便反序列化旧事件。 AggregateEventTypeId属性有助于将json反序列化为正确的类,只要编码器在转移/重构事件类时保持这些属性不变。

Newtonsoft自己的TypeNameHandling功能无法准确反序列化其名称已被重构的类。

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;
}
c# json json.net event-sourcing
1个回答
1
投票

在阅读评论中提供的链接后,我想出了这个解决方案。

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;
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.