Fixed #8: Forwards compatibility of enums
This commit is contained in:
parent
d37e593b78
commit
45c090d00d
90
Tapeti/Default/FallbackStringEnumConverter.cs
Normal file
90
Tapeti/Default/FallbackStringEnumConverter.cs
Normal 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<>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user