Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for serializing exceptions #505

Merged
merged 2 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions src/StreamJsonRpc.Tests/JsonRpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1911,6 +1911,65 @@ public async Task DisposeOnDisconnect_VsThreadingAsyncDisposable(bool throwFromD
Assert.True(server.IsDisposed);
}

[Fact]
public async Task SerializableExceptions()
{
// Create a full exception with inner exceptions. We have to throw so that its stacktrace is initialized.
Exception? exceptionToSend;
try
{
try
{
throw new InvalidOperationException("IOE test exception")
{
Data =
{
{ "someKey", "someValue" },
},
};
}
catch (InvalidOperationException inner)
{
throw new FileNotFoundException("FNF test exception", inner);
}
}
catch (FileNotFoundException outer)
{
exceptionToSend = outer;
}

await this.clientRpc.InvokeWithCancellationAsync(nameof(Server.SendException), new[] { exceptionToSend }, new[] { typeof(Exception) }, this.TimeoutToken);

// Make sure the exception is its own unique (deserialized) instance, but equal by value.
Assert.NotSame(this.server.ReceivedException, exceptionToSend);
AssertExceptionEquality(exceptionToSend, this.server.ReceivedException);
}

[Fact]
public async Task SerializableExceptions_NonExistant()
{
// Synthesize an exception message that refers to an exception type that does not exist.
var exceptionToSend = new LyingException("lying message");
await this.clientRpc.InvokeWithCancellationAsync(nameof(Server.SendException), new[] { exceptionToSend }, new[] { typeof(Exception) }, this.TimeoutToken);

// Make sure the exception is its own unique (deserialized) instance, but equal by value.
Assert.NotSame(this.server.ReceivedException, exceptionToSend);
AssertExceptionEquality(exceptionToSend, this.server.ReceivedException, compareType: false);

// Verify that the base exception type was created.
Assert.IsType<Exception>(this.server.ReceivedException);
}

[Fact]
public async Task SerializableExceptions_Null()
{
// Set this to a non-null value so when we assert null we know the RPC server method was in fact invoked.
this.server.ReceivedException = new InvalidOperationException();

await this.clientRpc.InvokeWithCancellationAsync(nameof(Server.SendException), new object?[] { null }, new[] { typeof(Exception) }, this.TimeoutToken);
Assert.Null(this.server.ReceivedException);
}

protected static Exception CreateExceptionToBeThrownByDeserializer() => new Exception("This exception is meant to be thrown.");

protected override void Dispose(bool disposing)
Expand Down Expand Up @@ -1942,6 +2001,29 @@ protected override Task CheckGCPressureAsync(Func<Task> scenario, int maxBytesAl
return base.CheckGCPressureAsync(scenario, maxBytesAllocated, iterations, allowedAttempts);
}

private static void AssertExceptionEquality(Exception? expected, Exception? actual, bool compareType = true)
{
Assert.Equal(expected is null, actual is null);
if (expected is null || actual is null)
{
return;
}

Assert.Equal(expected.Message, actual.Message);
Assert.Equal(expected.Data, actual.Data);
Assert.Equal(expected.HResult, actual.HResult);
Assert.Equal(expected.Source, actual.Source);
Assert.Equal(expected.HelpLink, actual.HelpLink);
Assert.Equal(expected.StackTrace, actual.StackTrace);

if (compareType)
{
Assert.Equal(expected.GetType(), actual.GetType());
}

AssertExceptionEquality(expected.InnerException, actual.InnerException);
}

private static void SendObject(Stream receivingStream, object jsonObject)
{
Requires.NotNull(receivingStream, nameof(receivingStream));
Expand Down Expand Up @@ -2049,6 +2131,8 @@ public class Server : BaseClass, IServer

public bool DelayAsyncMethodWithCancellation { get; set; }

internal Exception? ReceivedException { get; set; }

public static string ServerMethod(string argument)
{
return argument + "!";
Expand Down Expand Up @@ -2440,6 +2524,11 @@ public void ThrowRemoteInvocationException()
throw new LocalRpcException { ErrorCode = 2, ErrorData = new { myCustomData = "hi" } };
}

public void SendException(Exception? ex)
{
this.ReceivedException = ex;
}

internal void InternalMethod()
{
this.ServerMethodReached.Set();
Expand Down Expand Up @@ -2696,4 +2785,30 @@ private class ExceptionThrowingFormatter : JsonMessageFormatter, IJsonRpcMessage
base.Serialize(bufferWriter, message);
}
}

[Serializable]
private class LyingException : Exception
{
public LyingException(string message)
: base(message)
{
}

public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
// Arrange to change the ClassName value.
var scratch = new SerializationInfo(info.ObjectType, new FormatterConverter());
base.GetObjectData(scratch, context);

foreach (var entry in scratch)
{
if (entry.Name != "ClassName")
{
info.AddValue(entry.Name, entry.Value, entry.ObjectType);
}
}

info.AddValue("ClassName", "My.NonExistentException");
}
}
}
122 changes: 122 additions & 0 deletions src/StreamJsonRpc/JsonMessageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public JsonMessageFormatter(Encoding encoding)
new PipeReaderConverter(this),
new PipeWriterConverter(this),
new StreamConverter(this),
new ExceptionConverter(this),
},
};
}
Expand Down Expand Up @@ -1104,5 +1105,126 @@ public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer
writer.WriteValue(token);
}
}

private class JsonConverterFormatter : IFormatterConverter
{
private readonly JsonSerializer serializer;

internal JsonConverterFormatter(JsonSerializer serializer)
{
this.serializer = serializer;
}

public object Convert(object value, Type type) => ((JToken)value).ToObject(type, this.serializer);

public object Convert(object value, TypeCode typeCode)
{
return typeCode switch
{
TypeCode.Object => ((JToken)value).ToObject(typeof(object), this.serializer),
_ => ExceptionSerializationHelpers.Convert(this, value, typeCode),
};
}

public bool ToBoolean(object value) => ((JToken)value).ToObject<bool>(this.serializer);

public byte ToByte(object value) => ((JToken)value).ToObject<byte>(this.serializer);

public char ToChar(object value) => ((JToken)value).ToObject<char>(this.serializer);

public DateTime ToDateTime(object value) => ((JToken)value).ToObject<DateTime>(this.serializer);

public decimal ToDecimal(object value) => ((JToken)value).ToObject<decimal>(this.serializer);

public double ToDouble(object value) => ((JToken)value).ToObject<double>(this.serializer);

public short ToInt16(object value) => ((JToken)value).ToObject<short>(this.serializer);

public int ToInt32(object value) => ((JToken)value).ToObject<int>(this.serializer);

public long ToInt64(object value) => ((JToken)value).ToObject<long>(this.serializer);

public sbyte ToSByte(object value) => ((JToken)value).ToObject<sbyte>(this.serializer);

public float ToSingle(object value) => ((JToken)value).ToObject<float>(this.serializer);

public string ToString(object value) => ((JToken)value).ToObject<string>(this.serializer);

public ushort ToUInt16(object value) => ((JToken)value).ToObject<ushort>(this.serializer);

public uint ToUInt32(object value) => ((JToken)value).ToObject<uint>(this.serializer);

public ulong ToUInt64(object value) => ((JToken)value).ToObject<ulong>(this.serializer);
}

private class ExceptionConverter : JsonConverter<Exception?>
{
private readonly JsonMessageFormatter formatter;

internal ExceptionConverter(JsonMessageFormatter formatter)
{
this.formatter = formatter;
}

public override Exception? ReadJson(JsonReader reader, Type objectType, Exception? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}

if (reader.TokenType != JsonToken.StartObject)
{
throw new InvalidOperationException("Expected a StartObject token.");
}

SerializationInfo? info = new SerializationInfo(objectType, new JsonConverterFormatter(serializer));
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject)
{
break;
}

if (reader.TokenType == JsonToken.PropertyName)
{
string name = (string)reader.Value;
if (!reader.Read())
{
throw new EndOfStreamException();
}

JToken? value = reader.TokenType == JsonToken.Null ? null : JToken.Load(reader);
info.AddValue(name, value);
}
else
{
throw new InvalidOperationException("Expected PropertyName token but encountered: " + reader.TokenType);
}
}

return ExceptionSerializationHelpers.Deserialize<Exception>(info, this.formatter.rpc?.TraceSource);
}

public override void WriteJson(JsonWriter writer, Exception? value, JsonSerializer serializer)
{
if (value is null)
{
writer.WriteNull();
return;
}

SerializationInfo info = new SerializationInfo(value.GetType(), new JsonConverterFormatter(serializer));
ExceptionSerializationHelpers.Serialize(value, info);
writer.WriteStartObject();
foreach (SerializationEntry element in info)
{
writer.WritePropertyName(element.Name);
serializer.Serialize(writer, element.Value);
}

writer.WriteEndObject();
}
}
}
}
5 changes: 5 additions & 0 deletions src/StreamJsonRpc/JsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,11 @@ public enum TraceEvents
/// An outgoing RPC message was not sent due to an exception, possibly a serialization failure.
/// </summary>
TransmissionFailed,

/// <summary>
/// An incoming <see cref="Exception"/> cannot be deserialized to its original type because the type could not be loaded.
/// </summary>
ExceptionTypeNotFound,
}

/// <summary>
Expand Down
Loading