Skip to content

Commit

Permalink
Try to keep useful information from the non-serializable exceptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
yallie committed Nov 19, 2024
1 parent 2c6ea75 commit 9d58153
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 15 deletions.
41 changes: 41 additions & 0 deletions CoreRemoting.Tests/RpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using CoreRemoting.Serialization;
using CoreRemoting.Tests.ExternalTypes;
using CoreRemoting.Tests.Tools;
using Xunit;
Expand Down Expand Up @@ -468,5 +469,45 @@ await proxy.ErrorAsync(nameof(ErrorAsync_method_throws_Exception))))
_serverFixture.ServerErrorCount = 0;
}
}

[Fact]
public void NonSerializableError_method_throws_Exception()
{
try
{
using var client = new RemotingClient(new ClientConfig()
{
ConnectionTimeout = 5,
InvocationTimeout = 5,
SendTimeout = 5,
MessageEncryption = false,
ServerPort = _serverFixture.Server.Config.NetworkPort
});

client.Connect();

var proxy = client.CreateProxy<ITestService>();
var ex = Assert.Throws<RemoteInvocationException>(() =>
proxy.NonSerializableError("Hello", "Serializable", "World"))
.GetInnermostException();

Assert.NotNull(ex);
Assert.IsType<SerializableException>(ex);

if (ex is SerializableException sx)
{
Assert.Equal("NonSerializable", sx.SourceTypeName);
Assert.Equal("Hello", ex.Message);
Assert.Equal("Serializable", ex.Data["Serializable"]);
Assert.Equal("World", ex.Data["World"]);
Assert.NotNull(ex.StackTrace);
}
}
finally
{
// reset the error counter for other tests
_serverFixture.ServerErrorCount = 0;
}
}
}
}
14 changes: 0 additions & 14 deletions CoreRemoting.Tests/Tools/ExceptionExtensions.cs

This file was deleted.

2 changes: 2 additions & 0 deletions CoreRemoting.Tests/Tools/ITestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,7 @@ public interface ITestService : IBaseService
void Error(string text);

Task ErrorAsync(string text);

void NonSerializableError(string text, params object[] data);
}
}
18 changes: 18 additions & 0 deletions CoreRemoting.Tests/Tools/TestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@ public async Task ErrorAsync(string text)
await Task.Delay(1);
Error(text);
}

private class NonSerializable : Exception
{
public NonSerializable(string message)
: base(message)
{
}
}

public void NonSerializableError(string text, params object[] data)
{
var ex = new NonSerializable(text);

foreach (var item in data)
ex.Data[item] = item;

throw ex;
}
}
}
2 changes: 1 addition & 1 deletion CoreRemoting/RemotingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ private void ProcessRpcMessage(WireMessage request)
serverRpcContext.Exception =
new RemoteInvocationException(
message: ex.Message,
innerEx: ex.GetType().IsSerializable ? ex : null);
innerEx: ex.ToSerializable());

((RemotingServer)_server).OnAfterCall(serverRpcContext);

Expand Down
65 changes: 65 additions & 0 deletions CoreRemoting/Serialization/ExceptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Linq;

namespace CoreRemoting.Serialization;

/// <summary>
/// Extension methods for the exception classes.
/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// Checks whether the exception is serializable.
/// </summary>
public static bool IsSerializable(this Exception ex) => ex switch
{
null => true,

AggregateException agg =>
agg.InnerExceptions.All(ix => ix.IsSerializable()) &&
agg.InnerException.IsSerializable() &&
agg.GetType().IsSerializable,

_ => ex.GetType().IsSerializable &&
ex.InnerException.IsSerializable()
};

/// <summary>
/// Converts the non-serializable exception to a serializable copy.
/// </summary>
public static Exception ToSerializable(this Exception ex) =>
ex.IsSerializable() ? ex :
new SerializableException(ex.GetType().Name, ex.Message,
ex.InnerException.ToSerializable(), ex.StackTrace)
.CopyDataFrom(ex);

/// <summary>
/// Copies all exception data slots from the original exception.
/// </summary>
/// <typeparam name="TException">Exception type.</typeparam>
/// <param name="ex">Target exception.</param>
/// <param name="original">Original exception.</param>
/// <returns>Modified target exception.</returns>
public static TException CopyDataFrom<TException>(this TException ex, Exception original)
where TException : Exception
{
if (ex == null || original == null)
return ex;

foreach (var key in original.Data.Keys)
ex.Data[key] = original.Data[key];

return ex;
}

/// <summary>
/// Returns the most inner exception.
/// </summary>
public static Exception GetInnermostException(this Exception ex)
{
while (ex?.InnerException != null)
ex = ex.InnerException;

return ex;
}
}
103 changes: 103 additions & 0 deletions CoreRemoting/Serialization/SerializableException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Runtime.Serialization;

namespace CoreRemoting.Serialization;

/// <summary>
/// Serializable exception replacement for non-serializable exceptions.
/// </summary>
[Serializable]
public class SerializableException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="typeName">Source exception type name.</param>
/// <param name="message">The message.</param>
public SerializableException(string typeName, string message)
: base(message)
{
SourceTypeName = typeName;
}

/// <summary>
/// Initializes a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="typeName">Source exception type name.</param>
/// <param name="message">The message.</param>
/// <param name="innerException">The inner exception.</param>
public SerializableException(string typeName, string message, Exception innerException)
: base(message, innerException)
{
SourceTypeName = typeName;
}

/// <summary>
/// Initializes a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="typeName">Source exception type name.</param>
/// <param name="message">The message.</param>
/// <param name="newStackTrace">The new stack trace.</param>
public SerializableException(string typeName, string message, string newStackTrace)
: base(message)
{
SourceTypeName = typeName;
stackTrace = newStackTrace;
}

/// <summary>
/// Initializes a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="typeName">Source exception type name.</param>
/// <param name="message">The message.</param>
/// <param name="innerException">The inner exception.</param>
/// <param name="newStackTrace">The new stack trace.</param>
public SerializableException(string typeName, string message, Exception innerException, string newStackTrace)
: base(message, innerException)
{
SourceTypeName = typeName;
stackTrace = newStackTrace;
}

/// <summary>
/// Initializes a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="info">The object that holds the serialized object data.</param>
/// <param name="context">The contextual information about the source or destination.</param>
protected SerializableException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
stackTrace = info.GetString("MyStackTrace");
SourceTypeName = info.GetString("SourceTypeName");
}

/// <summary>
/// Sets the <see cref="T:System.Runtime.Serialization.SerializationInfo" /> with information about the exception.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds
/// the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains
/// contextual information about the source or destination.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("MyStackTrace", stackTrace);
info.AddValue("SourceTypeName", SourceTypeName);
}

private string stackTrace;

/// <summary>
/// Gets a string representation of the immediate frames on the call stack.
/// </summary>
/// <returns>A string that describes the immediate frames of the call stack.</returns>
/// <PermissionSet>
/// <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" PathDiscovery="*AllFiles*"/>
/// </PermissionSet>
public override string StackTrace => stackTrace ?? base.StackTrace;

/// <summary>
/// Gets the type name of source exception.
/// </summary>
public string SourceTypeName { get; private set; }
}

0 comments on commit 9d58153

Please sign in to comment.