1
0
mirror of synced 2024-09-28 19:56:09 +00:00

Fixed #8: Forwards compatibility of enums

This commit is contained in:
Mark van Renswoude 2019-01-28 11:30:24 +01:00
parent d37e593b78
commit 45c090d00d
3 changed files with 114 additions and 6 deletions

View File

@ -0,0 +1,90 @@
using System;
using System.Diagnostics;
using Newtonsoft.Json;
namespace Tapeti.Default
{
/// <summary>
/// Converts an <see cref="Enum"/> 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.
/// </summary>
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<>);
}
}
}

View File

@ -3,7 +3,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using RabbitMQ.Client; using RabbitMQ.Client;
namespace Tapeti.Default namespace Tapeti.Default
@ -25,7 +24,7 @@ namespace Tapeti.Default
NullValueHandling = NullValueHandling.Ignore 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"); throw new ArgumentException($"{ClassTypeHeader} header not present");
var messageType = deserializedTypeNames.GetOrAdd(Encoding.UTF8.GetString((byte[])typeName), DeserializeTypeName); 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);
} }

View File

@ -80,12 +80,16 @@ namespace Test
return flowProvider.YieldWithParallelRequest() return flowProvider.YieldWithParallelRequest()
.AddRequestSync<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(new PoloConfirmationRequestMessage .AddRequestSync<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(new PoloConfirmationRequestMessage
{ {
StoredInState = StateTestGuid StoredInState = StateTestGuid,
EnumValue = TestEnum.Value1,
}, HandlePoloConfirmationResponse1) }, HandlePoloConfirmationResponse1)
.AddRequestSync<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(new PoloConfirmationRequestMessage .AddRequestSync<PoloConfirmationRequestMessage, PoloConfirmationResponseMessage>(new PoloConfirmationRequestMessage
{ {
StoredInState = StateTestGuid StoredInState = StateTestGuid,
EnumValue = TestEnum.Value2,
OptionalEnumValue = TestEnum.Value1
}, HandlePoloConfirmationResponse2) }, HandlePoloConfirmationResponse2)
.YieldSync(ContinuePoloConfirmation); .YieldSync(ContinuePoloConfirmation);
@ -127,7 +131,9 @@ namespace Test
return new PoloConfirmationResponseMessage 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))] [Request(Response = typeof(PoloMessage))]
public class MarcoMessage public class MarcoMessage
{ {
@ -157,6 +170,9 @@ namespace Test
{ {
[Required] [Required]
public Guid StoredInState { get; set; } public Guid StoredInState { get; set; }
public TestEnum EnumValue;
public TestEnum? OptionalEnumValue;
} }
@ -164,5 +180,8 @@ namespace Test
{ {
[Required] [Required]
public Guid ShouldMatchState { get; set; } public Guid ShouldMatchState { get; set; }
public TestEnum EnumValue;
public TestEnum? OptionalEnumValue;
} }
} }