diff --git a/src/NServiceBus.Core.Tests/Serializers/XML/SerializingArrayTests.cs b/src/NServiceBus.Core.Tests/Serializers/XML/SerializingArrayTests.cs index 5a70344499f..b1c1e8c799e 100644 --- a/src/NServiceBus.Core.Tests/Serializers/XML/SerializingArrayTests.cs +++ b/src/NServiceBus.Core.Tests/Serializers/XML/SerializingArrayTests.cs @@ -17,18 +17,24 @@ public MessageWithArray(Guid sagaId, int[] someInts) SomeInts = someInts; } } - + [Serializable] public class MessageWithArrayAndNoDefaultCtor { public Guid SagaId { get; set; } public string[] SomeWords { get; set; } - } + } + + [Serializable] + public class MessageWithNullableArray + { + public Guid SagaId { get; set; } + public int?[] SomeInts { get; set; } + } [TestFixture] public class SerializingArrayTests { - [Test] public void CanDeserializeXmlWithWhitespace() { @@ -71,6 +77,7 @@ public void CanSerializeAndBack() Assert.AreEqual(1234, result.SomeInts[0]); Assert.AreEqual(5323, result.SomeInts[1]); } + [Test] public void CanSerializeMessageWithNullArray() { @@ -83,6 +90,7 @@ public void CanSerializeMessageWithNullArray() Assert.IsNull(result.SomeWords); } + [Test] public void CanSerializeMessageWithEmptyArray() { @@ -95,5 +103,74 @@ public void CanSerializeMessageWithEmptyArray() Assert.AreEqual(result.SomeWords, new string[0]); } + + [Test] + public void CanSerializeNullableArrayWithNullString() + { + var message = new MessageWithNullableArray + { + SagaId = Guid.Empty, + SomeInts = new int?[] { null } + }; + + using (var stream = new MemoryStream()) + { + SerializerFactory.Create().Serialize(message, stream); + stream.Position = 0; + var reader = new StreamReader(stream); + var xml = reader.ReadToEnd().Replace("\r\n", "\n").Replace("\n", "\r\n"); + + Assert.AreEqual(@" + +00000000-0000-0000-0000-000000000000 + + +null + + +", xml); + } + } + + [Test] + public void CanDeserializeNullableArrayWithValueSetToNullString() + { + var xml = @" + +00000000-0000-0000-0000-000000000000 + +null + + +"; + var data = Encoding.UTF8.GetBytes(xml); + + using (var stream = new MemoryStream(data)) + { + var msgArray = SerializerFactory.Create().Deserialize(stream, new[] { typeof(MessageWithNullableArray) }); + var result = (MessageWithNullableArray)msgArray[0]; + + Assert.AreEqual(null, result.SomeInts[0]); + } + } + + [Test] + public void CanSerializeMessageWithNullableArray() + { + // Issue https://github.com/Particular/NServiceBus/issues/2706 + + var message = new MessageWithNullableArray + { + SomeInts = new int?[] { null, 1, null, 3, null } + }; + + var result = ExecuteSerializer.ForMessage(message); + + Assert.IsNull(result.SomeInts[0]); + Assert.AreEqual(1, result.SomeInts[1]); + Assert.IsNull(result.SomeInts[2]); + Assert.AreEqual(3, result.SomeInts[3]); + Assert.IsNull(result.SomeInts[4]); + } } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Serializers/XML/Deserializer.cs b/src/NServiceBus.Core/Serializers/XML/Deserializer.cs index 83bf4b395a4..17285bf3e5d 100644 --- a/src/NServiceBus.Core/Serializers/XML/Deserializer.cs +++ b/src/NServiceBus.Core/Serializers/XML/Deserializer.cs @@ -234,7 +234,6 @@ Type InferNodeType(XmlNode node, object parent) return mappedType; } - logger.Debug("Could not load " + typeName + ". Trying base types..."); foreach (var baseType in messageBaseTypes) { @@ -243,7 +242,7 @@ Type InferNodeType(XmlNode node, object parent) logger.Debug("Trying to deserialize message to " + baseType.FullName); return baseType; } - // ReSharper disable once EmptyGeneralCatchClause + // ReSharper disable once EmptyGeneralCatchClause catch { // intentionally swallow exception @@ -255,7 +254,7 @@ Type InferNodeType(XmlNode node, object parent) object GetObjectOfTypeFromNode(Type t, XmlNode node) { - if (t.IsSimpleType() || t == typeof(Uri)) + if (t.IsSimpleType() || t == typeof(Uri) || t.IsNullableType()) { return GetPropertyValue(t, node); } @@ -340,7 +339,7 @@ object GetPropertyValue(Type type, XmlNode n) var nullableType = typeof(Nullable<>).MakeGenericType(args); if (type == nullableType) { - if (text.ToLower() == "null") + if (text.Trim().ToLower() == "null") { return null; } diff --git a/src/NServiceBus.Core/Serializers/XML/Serializer.cs b/src/NServiceBus.Core/Serializers/XML/Serializer.cs index a7fbcb167e1..25bda73ab08 100644 --- a/src/NServiceBus.Core/Serializers/XML/Serializer.cs +++ b/src/NServiceBus.Core/Serializers/XML/Serializer.cs @@ -74,77 +74,81 @@ StringBuilder SerializeMessage(object message) static string FormatAsString(object value) { + if (value == null) + { + return "null"; + } if (value is bool) { - return XmlConvert.ToString((bool) value); + return XmlConvert.ToString((bool)value); } if (value is byte) { - return XmlConvert.ToString((byte) value); + return XmlConvert.ToString((byte)value); } if (value is char) { - return Escape((char) value); + return Escape((char)value); } if (value is double) { - return XmlConvert.ToString((double) value); + return XmlConvert.ToString((double)value); } if (value is ulong) { - return XmlConvert.ToString((ulong) value); + return XmlConvert.ToString((ulong)value); } if (value is uint) { - return XmlConvert.ToString((uint) value); + return XmlConvert.ToString((uint)value); } if (value is ushort) { - return XmlConvert.ToString((ushort) value); + return XmlConvert.ToString((ushort)value); } if (value is long) { - return XmlConvert.ToString((long) value); + return XmlConvert.ToString((long)value); } if (value is int) { - return XmlConvert.ToString((int) value); + return XmlConvert.ToString((int)value); } if (value is short) { - return XmlConvert.ToString((short) value); + return XmlConvert.ToString((short)value); } if (value is sbyte) { - return XmlConvert.ToString((sbyte) value); + return XmlConvert.ToString((sbyte)value); } if (value is decimal) { - return XmlConvert.ToString((decimal) value); + return XmlConvert.ToString((decimal)value); } if (value is float) { - return XmlConvert.ToString((float) value); + return XmlConvert.ToString((float)value); } if (value is Guid) { - return XmlConvert.ToString((Guid) value); + return XmlConvert.ToString((Guid)value); } if (value is DateTime) { - return XmlConvert.ToString((DateTime) value, XmlDateTimeSerializationMode.RoundtripKind); + return XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.RoundtripKind); } if (value is DateTimeOffset) { - return XmlConvert.ToString((DateTimeOffset) value); + return XmlConvert.ToString((DateTimeOffset)value); } if (value is TimeSpan) { - return XmlConvert.ToString((TimeSpan) value); + return XmlConvert.ToString((TimeSpan)value); } if (value is string) { - return Escape(value as string); + return Escape((string)value); } return Escape(value.ToString()); @@ -166,15 +170,19 @@ static string Escape(char c) case '<': ss = "<"; break; + case '>': ss = ">"; break; + case '"': ss = """; break; + case '\'': ss = "'"; break; + case '&': ss = "&"; break; @@ -186,7 +194,7 @@ static string Escape(char c) } else { - return String.Format("&#x{0:X};", (int) c); + return String.Format("&#x{0:X};", (int)c); } //Should not get here but just in case! @@ -218,15 +226,19 @@ static string Escape(string stringToEscape) case '<': ss = "<"; break; + case '>': ss = ">"; break; + case '"': ss = """; break; + case '\'': ss = "'"; break; + case '&': ss = "&"; break; @@ -257,7 +269,7 @@ static string Escape(string stringToEscape) builder.Append(stringToEscape, startIndex, i - startIndex); } startIndex = i + 1; - builder.AppendFormat("&#x{0:X};", (int) c); + builder.AppendFormat("&#x{0:X};", (int)c); } } @@ -318,7 +330,7 @@ void WriteObject(string name, Type type, object value, StringBuilder builder, bo { var element = name; - if (type == typeof(object) && (value.GetType().IsSimpleType())) + if (type == typeof(object) && value.GetType().IsSimpleType()) { if (!namespacesToAdd.Contains(value.GetType())) { @@ -332,7 +344,6 @@ void WriteObject(string name, Type type, object value, StringBuilder builder, bo return; } - if (useNS) { var @namespace = InitializeNamespaces(value); @@ -353,6 +364,11 @@ void Write(StringBuilder builder, Type t, object obj) { if (obj == null) { + // For null entries in a nullable array + // See https://github.com/Particular/NServiceBus/issues/2706 + if (t.IsNullableType()) + builder.Append("null"); + return; } @@ -402,7 +418,7 @@ void WriteEntry(string name, Type type, object value, StringBuilder builder) if (typeof(XContainer).IsAssignableFrom(type)) { - var container = (XContainer) value; + var container = (XContainer)value; if (SkipWrappingRawXml) { builder.AppendFormat("{0}\n", container); @@ -427,7 +443,7 @@ void WriteEntry(string name, Type type, object value, StringBuilder builder) if (type == typeof(byte[])) { - var base64String = Convert.ToBase64String((byte[]) value); + var base64String = Convert.ToBase64String((byte[])value); builder.Append(base64String); } else @@ -459,8 +475,7 @@ void WriteEntry(string name, Type type, object value, StringBuilder builder) } } - - foreach (var obj in ((IEnumerable) value)) + foreach (var obj in ((IEnumerable)value)) { if (obj != null && obj.GetType().IsSimpleType()) { diff --git a/src/NServiceBus.Core/Utils/Reflection/ExtensionMethods.cs b/src/NServiceBus.Core/Utils/Reflection/ExtensionMethods.cs index b24c1d3f95b..36a82601ed4 100644 --- a/src/NServiceBus.Core/Utils/Reflection/ExtensionMethods.cs +++ b/src/NServiceBus.Core/Utils/Reflection/ExtensionMethods.cs @@ -37,6 +37,16 @@ public static bool IsSimpleType(this Type type) type.IsEnum); } + public static bool IsNullableType(this Type type) + { + var args = type.GetGenericArguments(); + if (args.Length == 1 && args[0].IsValueType) + { + return type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + return false; + } + /// /// Takes the name of the given type and makes it friendly for serialization /// by removing problematic characters.