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

Client should be able to receive translated exception from server #106

Merged
merged 4 commits into from
Dec 10, 2024
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
71 changes: 71 additions & 0 deletions CoreRemoting.Tests/ExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Reflection;
using CoreRemoting.Serialization;
using Xunit;

namespace CoreRemoting.Tests
{
public class ExceptionTests
{
/// <summary>
/// Private non-serializable exception class.
/// </summary>
class NonSerializableException : Exception
{
public NonSerializableException()
: this("This exception is not serializable")
{
}

public NonSerializableException(string message, Exception inner = null)
: base(message, inner)
{
}
}

[Fact]
public void Exception_can_be_checked_if_it_is_serializable()
{
Assert.True(new Exception().IsSerializable());
Assert.False(new NonSerializableException().IsSerializable());
Assert.True(new Exception("Hello", new Exception()).IsSerializable());
Assert.False(new Exception("Goodbye", new NonSerializableException()).IsSerializable());
}

[Fact]
public void Exception_can_be_turned_to_serializable()
{
var slotName = "SomeData";
var ex = new Exception("Bang!", new NonSerializableException("Zoom!"));
ex.Data[slotName] = DateTime.Now.ToString();
ex.InnerException.Data[slotName] = DateTime.Today.Ticks;
Assert.False(ex.IsSerializable());

var sx = ex.ToSerializable();
Assert.True(sx.IsSerializable());
Assert.NotSame(ex, sx);
Assert.NotSame(ex.InnerException, sx.InnerException);

Assert.Equal(ex.Message, sx.Message);
Assert.Equal(ex.Data[slotName], sx.Data[slotName]);
Assert.Equal(ex.InnerException.Message, sx.InnerException.Message);
Assert.Equal(ex.InnerException.Data[slotName], sx.InnerException.Data[slotName]);
}

[Fact]
public void SkipTargetInvocationException_returns_the_first_meaningful_inner_exception()
{
// the first meaningful exception
var ex = new Exception("Hello");
var tex = new TargetInvocationException(ex);
Assert.Equal(ex, tex.SkipTargetInvocationExceptions());

// no inner exceptions, return as is
tex = new TargetInvocationException(null);
Assert.Equal(tex, tex.SkipTargetInvocationExceptions());

// null, return as is
Assert.Null(default(Exception).SkipTargetInvocationExceptions());
}
}
}
47 changes: 47 additions & 0 deletions CoreRemoting.Tests/RpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,53 @@ public void NonSerializableError_method_throws_Exception()
}
}

[Fact]
public void AfterCall_event_handler_can_translate_exceptions_to_improve_diagnostics()
{
// replace cryptic database error report with a user-friendly error message
void AfterCall(object sender, ServerRpcContext ctx)
{
var errorMsg = ctx.Exception?.Message ?? string.Empty;
if (errorMsg.StartsWith("23503:"))
ctx.Exception = new Exception("Deleting clients is not allowed.",
ctx.Exception);
}

_serverFixture.Server.AfterCall += AfterCall;
try
{
using var client = new RemotingClient(new ClientConfig()
{
ConnectionTimeout = 5,
InvocationTimeout = 5,
SendTimeout = 5,
Channel = ClientChannel,
MessageEncryption = false,
ServerPort = _serverFixture.Server.Config.NetworkPort
});

client.Connect();

var dbError = "23503: delete from table 'clients' violates " +
"foreign key constraint 'order_client_fk' on table 'orders'";

// simulate a database error on the server-side
var proxy = client.CreateProxy<ITestService>();
var ex = Assert.Throws<Exception>(() => proxy.Error(dbError));

Assert.NotNull(ex);
Assert.Equal("Deleting clients is not allowed.", ex.Message);
Assert.NotNull(ex.InnerException);
Assert.Equal(dbError, ex.InnerException.Message);
}
finally
{
// reset the error counter for other tests
_serverFixture.ServerErrorCount = 0;
_serverFixture.Server.AfterCall -= AfterCall;
}
}

[Fact]
public void Failing_component_constructor_throws_RemoteInvocationException()
{
Expand Down
12 changes: 6 additions & 6 deletions CoreRemoting/ClientRpcContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ internal ClientRpcContext()
UniqueCallKey = Guid.NewGuid();
WaitHandle = new EventWaitHandle(initialState: false, EventResetMode.ManualReset);
}

/// <summary>
/// Gets the unique key of RPC call.
/// </summary>
public Guid UniqueCallKey { get; }

/// <summary>
/// Gets or sets the result message, that was received from server after the call was invoked on server side.
/// </summary>
public MethodCallResultMessage ResultMessage { get; set; }

/// <summary>
/// Gets or sets whether this RPC call is in error state.
/// </summary>
Expand All @@ -36,13 +36,13 @@ internal ClientRpcContext()
/// <summary>
/// Gets or sets an exception that describes an error that occurred on server side RPC invocation.
/// </summary>
public RemoteInvocationException RemoteException { get; set; }
public Exception RemoteException { get; set; }

/// <summary>
/// Gets a wait handle that is set, when the response of this RPC call is received from server.
/// </summary>
public EventWaitHandle WaitHandle { get; } // TODO: replace with a Task?

/// <summary>
/// Frees managed resources.
/// </summary>
Expand Down
Loading
Loading