Skip to content

Commit 90457d0

Browse files
authored
Add assembly version tolerance in serialization (#82)
Default to not including the Assembly Version and PublicKeyToken when serializing to allow seamless assembly versioning.
1 parent f431e2b commit 90457d0

File tree

3 files changed

+334
-30
lines changed

3 files changed

+334
-30
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using System.Text;
5+
using Newtonsoft.Json.Linq;
6+
using Xunit;
7+
8+
namespace ReactiveDomain.Foundation.Tests
9+
{
10+
#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
11+
public class TestObject
12+
{
13+
public string Data1;
14+
public string Data2 { get; set; }
15+
public override bool Equals(object obj) {
16+
return Equals(obj as TestObject);
17+
}
18+
public bool Equals(TestObject other) {
19+
if (other == null) return false;
20+
return string.CompareOrdinal(Data1, other.Data1) == 0 &&
21+
string.CompareOrdinal(Data2, other.Data2) == 0;
22+
}
23+
}
24+
public class TestObject2
25+
{
26+
public string Data2 { get; set; }
27+
public string Data3;
28+
public override bool Equals(object obj) {
29+
return Equals(obj as TestObject2);
30+
}
31+
public bool Equals(TestObject2 other) {
32+
if (other == null) return false;
33+
return string.CompareOrdinal(Data3, other.Data3) == 0 &&
34+
string.CompareOrdinal(Data2, other.Data2) == 0;
35+
}
36+
}
37+
#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
38+
39+
public class when_serializing_with_json_message_serializer
40+
{
41+
TestObject testObject = new TestObject { Data1 = "blaa", Data2 = "more blaa" };
42+
[Fact]
43+
public void can_serialize_objects() {
44+
45+
var serializer = new JsonMessageSerializer();
46+
var eventData = serializer.Serialize(testObject);
47+
var deserialized = serializer.Deserialize(eventData);
48+
Assert.IsType<TestObject>(deserialized);
49+
var testObject2 = (TestObject)deserialized;
50+
Assert.True(testObject.Equals(testObject2));
51+
52+
serializer.FullyQualify = true;
53+
eventData = serializer.Serialize(testObject);
54+
deserialized = serializer.Deserialize(eventData);
55+
Assert.IsType<TestObject>(deserialized);
56+
var testObject3 = (TestObject)deserialized;
57+
Assert.True(testObject.Equals(testObject3));
58+
59+
}
60+
61+
[Fact]
62+
public void can_write_qualified_name_header() {
63+
var serializer = new JsonMessageSerializer();
64+
65+
var eventData = serializer.Serialize(testObject);
66+
var clrQualifiedName = (string)JObject.Parse(Encoding.UTF8.GetString(eventData.Metadata))
67+
.Property(serializer.EventClrQualifiedTypeHeader).Value;
68+
var partialName = $"{testObject.GetType().FullName},{testObject.GetType().Assembly.GetName()}";
69+
Assert.True(string.CompareOrdinal(clrQualifiedName, partialName) == 0);
70+
71+
serializer.FullyQualify = true;
72+
eventData = serializer.Serialize(testObject);
73+
clrQualifiedName = (string)JObject.Parse(Encoding.UTF8.GetString(eventData.Metadata))
74+
.Property(serializer.EventClrQualifiedTypeHeader).Value;
75+
Assert.True(string.CompareOrdinal(clrQualifiedName, testObject.GetType().AssemblyQualifiedName) == 0);
76+
}
77+
78+
[Fact]
79+
public void can_write_typename_header() {
80+
var serializer = new JsonMessageSerializer();
81+
82+
var eventData = serializer.Serialize(testObject);
83+
var typeName = (string)JObject.Parse(Encoding.UTF8.GetString(eventData.Metadata))
84+
.Property(serializer.EventClrTypeHeader).Value;
85+
Assert.True(string.CompareOrdinal(typeName, testObject.GetType().Name) == 0);
86+
}
87+
[Fact]
88+
public void can_write_custom_header() {
89+
var serializer = new JsonMessageSerializer();
90+
var headerName = "MyHeader";
91+
var HeaderData = "my header data";
92+
var headers = new Dictionary<string, object> { { headerName, HeaderData } };
93+
var eventData = serializer.Serialize(testObject, headers);
94+
var customHeaderData = (string)JObject.Parse(Encoding.UTF8.GetString(eventData.Metadata))
95+
.Property(headerName).Value;
96+
Assert.True(string.CompareOrdinal(HeaderData, customHeaderData) == 0);
97+
}
98+
99+
[Fact]
100+
public void can_overwrite_standard_headers() {
101+
var serializer = new JsonMessageSerializer();
102+
var headerName = serializer.EventClrTypeHeader;
103+
var headerData = "my clr type header";
104+
var headers = new Dictionary<string, object> { { headerName, headerData } };
105+
var eventData = serializer.Serialize(testObject, headers);
106+
var headerValue = (string)JObject.Parse(Encoding.UTF8.GetString(eventData.Metadata))
107+
.Property(headerName).Value;
108+
Assert.True(string.CompareOrdinal(headerData, headerValue) == 0);
109+
110+
headerName = serializer.EventClrQualifiedTypeHeader;
111+
headerData = typeof(TestObject2).AssemblyQualifiedName;
112+
headers = new Dictionary<string, object> { { headerName, headerData } };
113+
eventData = serializer.Serialize(testObject, headers);
114+
headerValue = (string)JObject.Parse(Encoding.UTF8.GetString(eventData.Metadata))
115+
.Property(headerName).Value;
116+
Assert.True(string.CompareOrdinal(headerData, headerValue) == 0);
117+
}
118+
[Fact]
119+
public void serializer_will_use_overriden_headers_on_deserialize() {
120+
var serializer = new JsonMessageSerializer();
121+
122+
//n.b. setting header to different type than testObject
123+
var headerName = serializer.EventClrQualifiedTypeHeader;
124+
var headerData = $"{typeof(TestObject2).FullName},{typeof(TestObject2).Assembly.GetName()}";
125+
var headers = new Dictionary<string, object> { { headerName, headerData } };
126+
var eventData = serializer.Serialize(testObject, headers);
127+
var deserialized = serializer.Deserialize(eventData);
128+
Assert.IsType<TestObject2>(deserialized);
129+
var testObject2 = (TestObject2)deserialized;
130+
Assert.True(string.CompareOrdinal(testObject.Data2, testObject2.Data2) == 0);
131+
}
132+
133+
[Fact]
134+
public void can_deserialize_to_a_specified_type() {
135+
var serializer = new JsonMessageSerializer();
136+
var eventData = serializer.Serialize(testObject);
137+
//n.b. testObject is type TestObject
138+
var deserialized = serializer.Deserialize(eventData, typeof(TestObject2));
139+
Assert.IsType<TestObject2>(deserialized);
140+
var testObject2 = (TestObject2)deserialized;
141+
Assert.True(string.CompareOrdinal(testObject.Data2, testObject2.Data2) == 0);
142+
143+
var newTestObject2 = serializer.Deserialize<TestObject2>(eventData);
144+
Assert.True(string.CompareOrdinal(testObject.Data2, newTestObject2.Data2) == 0);
145+
}
146+
147+
[Fact]
148+
public void can_throw_if_type_not_found() {
149+
var serializer = new JsonMessageSerializer();
150+
151+
//n.b. setting header to non existent assembly
152+
var headerName = serializer.EventClrQualifiedTypeHeader;
153+
var headerData = $"{typeof(TestObject2).FullName},dne-assembly";
154+
var headers = new Dictionary<string, object> {{headerName, headerData}};
155+
var eventData = serializer.Serialize(testObject, headers);
156+
//confirm type not found
157+
var deserialized = serializer.Deserialize(eventData);
158+
Assert.IsType<JObject>(deserialized);
159+
//request throw on type not found
160+
serializer.ThrowOnTypeNotFound = true;
161+
Assert.Throws<InvalidOperationException>(() => serializer.Deserialize(eventData));
162+
}
163+
164+
[Fact]
165+
public void can_override_target_assembly() {
166+
var serializer = new JsonMessageSerializer();
167+
168+
//n.b. setting header to non existent assembly
169+
var headerName = serializer.EventClrQualifiedTypeHeader;
170+
var headerData = $"{typeof(TestObject2).FullName},dne-assembly";
171+
var headers = new Dictionary<string, object> { { headerName, headerData } };
172+
var eventData = serializer.Serialize(testObject, headers);
173+
//confirm type not found
174+
var deserialized = serializer.Deserialize(eventData);
175+
Assert.IsType<JObject>(deserialized);
176+
//override assembly
177+
serializer.AssemblyOverride = Assembly.GetExecutingAssembly();
178+
deserialized = serializer.Deserialize(eventData);
179+
var testObject2 = (TestObject2)deserialized;
180+
Assert.True(string.CompareOrdinal(testObject.Data2, testObject2.Data2) == 0);
181+
}
182+
183+
[Fact]
184+
public void can_deserialize_from_either_fully_or_partially_qualified_names() {
185+
//n.b. the previous version of the JsonMessageSerializer was fully qualified, and
186+
//this test demonstrates cross compability with that version
187+
var obj1 = new TestObject {Data1 = Guid.NewGuid().ToString("N")};
188+
var obj2 = new TestObject {Data1 = Guid.NewGuid().ToString("N")};
189+
var fullyQualifiedSerializer = new JsonMessageSerializer {FullyQualify = true};
190+
var partiallyQualifiedSerializer = new JsonMessageSerializer();
191+
192+
//serialize with and without fully qualified names
193+
var eventData1 = fullyQualifiedSerializer.Serialize(obj1);
194+
var eventData2 = partiallyQualifiedSerializer.Serialize(obj2);
195+
//switch serializers
196+
var deserialized1 = partiallyQualifiedSerializer.Deserialize(eventData1);
197+
var deserialized2 = partiallyQualifiedSerializer.Deserialize(eventData2);
198+
199+
Assert.IsType<TestObject>(deserialized1);
200+
Assert.IsType<TestObject>(deserialized2);
201+
202+
var result1 = (TestObject) deserialized1;
203+
var result2 = (TestObject) deserialized2;
204+
205+
Assert.True(string.CompareOrdinal(result1.Data1, obj1.Data1) == 0);
206+
Assert.True(string.CompareOrdinal(result2.Data1, obj2.Data1) == 0);
207+
208+
}
209+
210+
}
211+
}

src/ReactiveDomain.Foundation/StreamStore/JsonMessageSerializer.cs

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,67 @@
66
using Newtonsoft.Json.Converters;
77
using Newtonsoft.Json.Linq;
88
using Newtonsoft.Json.Serialization;
9-
using ReactiveDomain.Messaging;
109

1110
// ReSharper disable once CheckNamespace
1211
namespace ReactiveDomain.Foundation
1312
{
1413
public class JsonMessageSerializer : IEventSerializer
1514
{
16-
private static readonly JsonSerializerSettings SerializerSettings;
17-
public const string EventClrQualifiedTypeHeader = "EventClrQualifiedTypeName";
18-
public const string EventClrTypeHeader = "EventClrTypeName";
1915

20-
static JsonMessageSerializer()
21-
{
22-
// SerializerSettings = Json.JsonSettings;
16+
public static readonly JsonSerializerSettings StandardSerializerSettings;
17+
public JsonSerializerSettings SerializerSettings {
18+
get => _serializerSettings ?? StandardSerializerSettings;
19+
set => _serializerSettings = value;
20+
}
21+
private JsonSerializerSettings _serializerSettings;
22+
public string EventClrQualifiedTypeHeader = "EventClrQualifiedTypeName";
23+
public string EventClrTypeHeader = "EventClrTypeName";
24+
public bool FullyQualify { get; set; }
25+
public Assembly AssemblyOverride { get; set; }
26+
public bool ThrowOnTypeNotFound { get; set; }
27+
28+
static JsonMessageSerializer() {
2329
var contractResolver = new DefaultContractResolver();
30+
#pragma warning disable 618
2431
contractResolver.DefaultMembersSearchFlags |= BindingFlags.NonPublic;
25-
SerializerSettings = new JsonSerializerSettings()
26-
{
32+
#pragma warning restore 618
33+
StandardSerializerSettings = new JsonSerializerSettings() {
2734
ContractResolver = contractResolver,
2835
TypeNameHandling = TypeNameHandling.Auto,
2936
DateFormatHandling = DateFormatHandling.IsoDateFormat,
3037
NullValueHandling = NullValueHandling.Ignore,
3138
DefaultValueHandling = DefaultValueHandling.Ignore,
32-
MissingMemberHandling = MissingMemberHandling.Ignore,
39+
MissingMemberHandling = MissingMemberHandling.Ignore,
3340
Converters = new JsonConverter[] { new StringEnumConverter() }
3441
};
3542
}
36-
public EventData Serialize(object @event, IDictionary<string, object> headers = null)
37-
{
3843

39-
if (headers == null)
40-
{
41-
headers = new Dictionary<string, object>();
42-
}
44+
/// <summary>
45+
/// Creates an default instance of the JsonSerializer for serializing and Deserializing Events from
46+
/// streams in EventStore. consumers are urged to create dedicated serializers that implement IEventSerializer
47+
/// for any custom needs, such as seamless event upgrades
48+
/// </summary>
49+
public JsonMessageSerializer(JsonMessageSerializerSettings messageSerializerSettings = null) {
50+
if (messageSerializerSettings == null) { messageSerializerSettings = new JsonMessageSerializerSettings(); }
51+
FullyQualify = messageSerializerSettings.FullyQualify;
52+
AssemblyOverride = messageSerializerSettings.AssemblyOverride;
53+
ThrowOnTypeNotFound = messageSerializerSettings.ThrowOnTypeNotFound;
54+
}
55+
public EventData Serialize(object @event, IDictionary<string, object> headers = null) {
56+
if (headers == null) { headers = new Dictionary<string, object>(); }
4357

44-
try
45-
{
58+
59+
if (!headers.ContainsKey(EventClrTypeHeader)) {
4660
headers.Add(EventClrTypeHeader, @event.GetType().Name);
47-
headers.Add(EventClrQualifiedTypeHeader, @event.GetType().AssemblyQualifiedName);
4861
}
49-
catch (Exception e)
50-
{
51-
var msg = e.Message;
5262

63+
if (!headers.ContainsKey(EventClrQualifiedTypeHeader)) {
64+
var qualifiedName = FullyQualify
65+
? @event.GetType().AssemblyQualifiedName
66+
: $"{@event.GetType().FullName},{@event.GetType().Assembly.GetName()}";
67+
headers.Add(EventClrQualifiedTypeHeader, qualifiedName);
5368
}
69+
5470
var metadata = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(headers, SerializerSettings));
5571
var dString = JsonConvert.SerializeObject(@event, SerializerSettings);
5672
var data = Encoding.UTF8.GetBytes(dString);
@@ -59,17 +75,55 @@ public EventData Serialize(object @event, IDictionary<string, object> headers =
5975
return new EventData(Guid.NewGuid(), typeName, true, data, metadata);
6076
}
6177

62-
public object Deserialize(IEventData @event)
63-
{
78+
public object Deserialize(IEventData @event) {
79+
var clrQualifiedName = (string)JObject.Parse(Encoding.UTF8.GetString(@event.Metadata))
80+
.Property(EventClrQualifiedTypeHeader).Value;
81+
return Deserialize(@event, clrQualifiedName);
82+
}
6483

65-
var eventClrTypeName = JObject.Parse(
66-
Encoding.UTF8.GetString(@event.Metadata)).Property(EventClrQualifiedTypeHeader).Value; // todo: fallback to using type name optionally
67-
return JsonConvert.DeserializeObject(
68-
Encoding.UTF8.GetString(@event.Data),
69-
Type.GetType((string)eventClrTypeName),
70-
SerializerSettings);
84+
public object Deserialize(IEventData @event, string fullyQualifiedName) {
85+
if (string.IsNullOrWhiteSpace(fullyQualifiedName)) {
86+
throw new ArgumentNullException(nameof(fullyQualifiedName), $"{fullyQualifiedName} cannot be null, empty, or whitespace");
87+
}
88+
var nameParts = fullyQualifiedName.Split(',');
89+
var fullName = nameParts[0];
90+
var assemblyName = nameParts[1];
91+
Type targetType;
92+
if (AssemblyOverride != null) {
93+
targetType = AssemblyOverride.GetType(fullName);
94+
if (ThrowOnTypeNotFound && targetType == null) {
95+
throw new InvalidOperationException($"Type not found for {fullName} in overridden assembly {AssemblyOverride.FullName}");
96+
}
97+
return Deserialize(@event, targetType);
98+
}
99+
if (FullyQualify) {
100+
targetType = Type.GetType(fullyQualifiedName);
101+
if (ThrowOnTypeNotFound && targetType == null) {
102+
throw new InvalidOperationException($"Type not found for {fullyQualifiedName}");
103+
}
104+
return Deserialize(@event, targetType);
105+
}
106+
107+
var partialQualifiedName = $"{fullName},{assemblyName}";
108+
targetType = Type.GetType(partialQualifiedName);
109+
if (ThrowOnTypeNotFound && targetType == null) {
110+
throw new InvalidOperationException($"Type not found for {partialQualifiedName}");
111+
}
112+
return Deserialize(@event, targetType);
71113
}
72114

115+
public object Deserialize(IEventData @event, Type type) {
116+
return JsonConvert.DeserializeObject(
117+
Encoding.UTF8.GetString(@event.Data),
118+
type,
119+
SerializerSettings);
120+
}
121+
public TEvent Deserialize<TEvent>(IEventData @event) {
122+
return (TEvent)JsonConvert.DeserializeObject(
123+
Encoding.UTF8.GetString(@event.Data),
124+
typeof(TEvent),
125+
SerializerSettings);
126+
}
73127

74128
}
75129
}

0 commit comments

Comments
 (0)