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;
}