From 9d581534f22883736e4bd2b0e30b082a6c785afc Mon Sep 17 00:00:00 2001 From: yallie Date: Wed, 20 Nov 2024 02:21:11 +0300 Subject: [PATCH] Try to keep useful information from the non-serializable exceptions. --- CoreRemoting.Tests/RpcTests.cs | 41 +++++++ .../Tools/ExceptionExtensions.cs | 14 --- CoreRemoting.Tests/Tools/ITestService.cs | 2 + CoreRemoting.Tests/Tools/TestService.cs | 18 +++ CoreRemoting/RemotingSession.cs | 2 +- .../Serialization/ExceptionExtensions.cs | 65 +++++++++++ .../Serialization/SerializableException.cs | 103 ++++++++++++++++++ 7 files changed, 230 insertions(+), 15 deletions(-) delete mode 100644 CoreRemoting.Tests/Tools/ExceptionExtensions.cs create mode 100644 CoreRemoting/Serialization/ExceptionExtensions.cs create mode 100644 CoreRemoting/Serialization/SerializableException.cs diff --git a/CoreRemoting.Tests/RpcTests.cs b/CoreRemoting.Tests/RpcTests.cs index b5df06c..6001d07 100644 --- a/CoreRemoting.Tests/RpcTests.cs +++ b/CoreRemoting.Tests/RpcTests.cs @@ -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; @@ -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(); + var ex = Assert.Throws(() => + proxy.NonSerializableError("Hello", "Serializable", "World")) + .GetInnermostException(); + + Assert.NotNull(ex); + Assert.IsType(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; + } + } } } \ No newline at end of file diff --git a/CoreRemoting.Tests/Tools/ExceptionExtensions.cs b/CoreRemoting.Tests/Tools/ExceptionExtensions.cs deleted file mode 100644 index c1df786..0000000 --- a/CoreRemoting.Tests/Tools/ExceptionExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace CoreRemoting.Tests.Tools; - -public static class ExceptionExtensions -{ - public static Exception GetInnermostException(this Exception ex) - { - while (ex?.InnerException != null) - ex = ex.InnerException; - - return ex; - } -} diff --git a/CoreRemoting.Tests/Tools/ITestService.cs b/CoreRemoting.Tests/Tools/ITestService.cs index 40d67a5..1512450 100644 --- a/CoreRemoting.Tests/Tools/ITestService.cs +++ b/CoreRemoting.Tests/Tools/ITestService.cs @@ -33,5 +33,7 @@ public interface ITestService : IBaseService void Error(string text); Task ErrorAsync(string text); + + void NonSerializableError(string text, params object[] data); } } \ No newline at end of file diff --git a/CoreRemoting.Tests/Tools/TestService.cs b/CoreRemoting.Tests/Tools/TestService.cs index af76c38..8a4ac66 100644 --- a/CoreRemoting.Tests/Tools/TestService.cs +++ b/CoreRemoting.Tests/Tools/TestService.cs @@ -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; + } } } \ No newline at end of file diff --git a/CoreRemoting/RemotingSession.cs b/CoreRemoting/RemotingSession.cs index 6902011..90c8724 100644 --- a/CoreRemoting/RemotingSession.cs +++ b/CoreRemoting/RemotingSession.cs @@ -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); diff --git a/CoreRemoting/Serialization/ExceptionExtensions.cs b/CoreRemoting/Serialization/ExceptionExtensions.cs new file mode 100644 index 0000000..8650318 --- /dev/null +++ b/CoreRemoting/Serialization/ExceptionExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; + +namespace CoreRemoting.Serialization; + +/// +/// Extension methods for the exception classes. +/// +public static class ExceptionExtensions +{ + /// + /// Checks whether the exception is serializable. + /// + 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() + }; + + /// + /// Converts the non-serializable exception to a serializable copy. + /// + public static Exception ToSerializable(this Exception ex) => + ex.IsSerializable() ? ex : + new SerializableException(ex.GetType().Name, ex.Message, + ex.InnerException.ToSerializable(), ex.StackTrace) + .CopyDataFrom(ex); + + /// + /// Copies all exception data slots from the original exception. + /// + /// Exception type. + /// Target exception. + /// Original exception. + /// Modified target exception. + public static TException CopyDataFrom(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; + } + + /// + /// Returns the most inner exception. + /// + public static Exception GetInnermostException(this Exception ex) + { + while (ex?.InnerException != null) + ex = ex.InnerException; + + return ex; + } +} diff --git a/CoreRemoting/Serialization/SerializableException.cs b/CoreRemoting/Serialization/SerializableException.cs new file mode 100644 index 0000000..ca3105b --- /dev/null +++ b/CoreRemoting/Serialization/SerializableException.cs @@ -0,0 +1,103 @@ +using System; +using System.Runtime.Serialization; + +namespace CoreRemoting.Serialization; + +/// +/// Serializable exception replacement for non-serializable exceptions. +/// +[Serializable] +public class SerializableException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// Source exception type name. + /// The message. + public SerializableException(string typeName, string message) + : base(message) + { + SourceTypeName = typeName; + } + + /// + /// Initializes a new instance of the class. + /// + /// Source exception type name. + /// The message. + /// The inner exception. + public SerializableException(string typeName, string message, Exception innerException) + : base(message, innerException) + { + SourceTypeName = typeName; + } + + /// + /// Initializes a new instance of the class. + /// + /// Source exception type name. + /// The message. + /// The new stack trace. + public SerializableException(string typeName, string message, string newStackTrace) + : base(message) + { + SourceTypeName = typeName; + stackTrace = newStackTrace; + } + + /// + /// Initializes a new instance of the class. + /// + /// Source exception type name. + /// The message. + /// The inner exception. + /// The new stack trace. + public SerializableException(string typeName, string message, Exception innerException, string newStackTrace) + : base(message, innerException) + { + SourceTypeName = typeName; + stackTrace = newStackTrace; + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that holds the serialized object data. + /// The contextual information about the source or destination. + protected SerializableException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + stackTrace = info.GetString("MyStackTrace"); + SourceTypeName = info.GetString("SourceTypeName"); + } + + /// + /// Sets the with information about the exception. + /// + /// The that holds + /// the serialized object data about the exception being thrown. + /// The that contains + /// contextual information about the source or destination. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("MyStackTrace", stackTrace); + info.AddValue("SourceTypeName", SourceTypeName); + } + + private string stackTrace; + + /// + /// Gets a string representation of the immediate frames on the call stack. + /// + /// A string that describes the immediate frames of the call stack. + /// + /// + /// + public override string StackTrace => stackTrace ?? base.StackTrace; + + /// + /// Gets the type name of source exception. + /// + public string SourceTypeName { get; private set; } +}