diff --git a/Tapeti/Default/FallbackStringEnumConverter.cs b/Tapeti/Default/FallbackStringEnumConverter.cs new file mode 100644 index 0000000..d4098c3 --- /dev/null +++ b/Tapeti/Default/FallbackStringEnumConverter.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using Newtonsoft.Json; + +namespace Tapeti.Default +{ + /// + /// Converts an to and from its name string value. If an unknown string value is encountered + /// it will translate to 0xDEADBEEF (-559038737) so it can be gracefully handled. + /// If you copy this value as-is to another message and try to send it, this converter will throw an exception. + /// + /// This converter is far simpler than the default StringEnumConverter, it assumes both sides use the same + /// enum and therefore skips the naming strategy. + /// + public class FallbackStringEnumConverter : JsonConverter + { + private readonly int invalidEnumValue; + + + public FallbackStringEnumConverter() + { + unchecked { invalidEnumValue = (int)0xDEADBEEF; } + } + + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + if ((int) value == invalidEnumValue) + throw new ArgumentException("Enum value was an unknown string value in an incoming message and can not be published in an outgoing message as-is"); + + var outputValue = Enum.GetName(value.GetType(), value); + writer.WriteValue(outputValue); + } + + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var isNullable = IsNullableType(objectType); + + if (reader.TokenType == JsonToken.Null) + { + if (!isNullable) + throw new JsonSerializationException($"Cannot convert null value to {objectType}"); + + return null; + } + + var actualType = isNullable ? Nullable.GetUnderlyingType(objectType) : objectType; + Debug.Assert(actualType != null, nameof(actualType) + " != null"); + + if (reader.TokenType != JsonToken.String) + throw new JsonSerializationException($"Unexpected token {reader.TokenType} when parsing enum"); + + var enumText = reader.Value.ToString(); + if (enumText == string.Empty && isNullable) + return null; + + try + { + return Enum.Parse(actualType, enumText); + } + catch (ArgumentException) + { + return Enum.ToObject(actualType, invalidEnumValue); + } + } + + + public override bool CanConvert(Type objectType) + { + var actualType = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType; + return actualType?.IsEnum ?? false; + } + + + private static bool IsNullableType(Type t) + { + if (t == null) + throw new ArgumentNullException(nameof(t)); + + return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); + } + } +} diff --git a/Tapeti/Default/JsonMessageSerializer.cs b/Tapeti/Default/JsonMessageSerializer.cs index 2aee24f..9cee002 100644 --- a/Tapeti/Default/JsonMessageSerializer.cs +++ b/Tapeti/Default/JsonMessageSerializer.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using RabbitMQ.Client; namespace Tapeti.Default @@ -25,7 +24,7 @@ namespace Tapeti.Default NullValueHandling = NullValueHandling.Ignore }; - serializerSettings.Converters.Add(new StringEnumConverter()); + serializerSettings.Converters.Add(new FallbackStringEnumConverter()); } @@ -52,7 +51,7 @@ namespace Tapeti.Default throw new ArgumentException($"{ClassTypeHeader} header not present"); var messageType = deserializedTypeNames.GetOrAdd(Encoding.UTF8.GetString((byte[])typeName), DeserializeTypeName); - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), messageType); + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), messageType, serializerSettings); } diff --git a/Test/MarcoController.cs b/Test/MarcoController.cs index 4ced01e..3368f59 100644 --- a/Test/MarcoController.cs +++ b/Test/MarcoController.cs @@ -80,12 +80,16 @@ namespace Test return flowProvider.YieldWithParallelRequest() .AddRequestSync(new PoloConfirmationRequestMessage { - StoredInState = StateTestGuid + StoredInState = StateTestGuid, + EnumValue = TestEnum.Value1, + }, HandlePoloConfirmationResponse1) .AddRequestSync(new PoloConfirmationRequestMessage { - StoredInState = StateTestGuid + StoredInState = StateTestGuid, + EnumValue = TestEnum.Value2, + OptionalEnumValue = TestEnum.Value1 }, HandlePoloConfirmationResponse2) .YieldSync(ContinuePoloConfirmation); @@ -127,7 +131,9 @@ namespace Test return new PoloConfirmationResponseMessage { - ShouldMatchState = message.StoredInState + ShouldMatchState = message.StoredInState, + EnumValue = message.EnumValue, + OptionalEnumValue = message.OptionalEnumValue }; } @@ -141,6 +147,13 @@ namespace Test } + public enum TestEnum + { + Value1, + Value2 + } + + [Request(Response = typeof(PoloMessage))] public class MarcoMessage { @@ -157,6 +170,9 @@ namespace Test { [Required] public Guid StoredInState { get; set; } + + public TestEnum EnumValue; + public TestEnum? OptionalEnumValue; } @@ -164,5 +180,8 @@ namespace Test { [Required] public Guid ShouldMatchState { get; set; } + + public TestEnum EnumValue; + public TestEnum? OptionalEnumValue; } }