From 9ccf94386d18e7ca330793be8122a4d4bdcb6c4e Mon Sep 17 00:00:00 2001 From: yallie Date: Tue, 10 Dec 2024 01:27:47 +0300 Subject: [PATCH 1/4] Implemented the SkipTargetInvocationExceptions extension method. --- CoreRemoting.Tests/ExceptionTests.cs | 51 +++++++++++++++++++ CoreRemoting/RemotingSession.cs | 4 ++ .../Serialization/ExceptionExtensions.cs | 16 ++++++ 3 files changed, 71 insertions(+) create mode 100644 CoreRemoting.Tests/ExceptionTests.cs diff --git a/CoreRemoting.Tests/ExceptionTests.cs b/CoreRemoting.Tests/ExceptionTests.cs new file mode 100644 index 0000000..972ac3b --- /dev/null +++ b/CoreRemoting.Tests/ExceptionTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Reflection; +using CoreRemoting.Serialization; +using Xunit; + +namespace CoreRemoting.Tests +{ + public class ExceptionTests + { + /// + /// Private non-serializable exception class. + /// + 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 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()); + } + } +} \ No newline at end of file diff --git a/CoreRemoting/RemotingSession.cs b/CoreRemoting/RemotingSession.cs index 58b90fe..248f12d 100644 --- a/CoreRemoting/RemotingSession.cs +++ b/CoreRemoting/RemotingSession.cs @@ -472,6 +472,8 @@ private void ProcessRpcMessage(WireMessage request) } catch (Exception ex) { + ex = ex.SkipTargetInvocationExceptions(); + serverRpcContext.Exception = new RemoteInvocationException( message: ex.Message, @@ -539,6 +541,8 @@ private void ProcessRpcMessage(WireMessage request) } catch (Exception ex) { + ex = ex.SkipTargetInvocationExceptions(); + serverRpcContext.Exception = new RemoteInvocationException( message: ex.Message, diff --git a/CoreRemoting/Serialization/ExceptionExtensions.cs b/CoreRemoting/Serialization/ExceptionExtensions.cs index c29cfbf..93bdecc 100644 --- a/CoreRemoting/Serialization/ExceptionExtensions.cs +++ b/CoreRemoting/Serialization/ExceptionExtensions.cs @@ -1,6 +1,7 @@ using Castle.MicroKernel.ComponentActivator; using System; using System.Linq; +using System.Reflection; namespace CoreRemoting.Serialization; @@ -56,6 +57,21 @@ public static TException CopyDataFrom(this TException ex, Exception return ex; } + /// + /// Skips the uninformative target invocation exceptions. + /// + /// The exception to process. + public static Exception SkipTargetInvocationExceptions(this Exception ex) + { + while (ex is TargetInvocationException && ex.InnerException != null) + { + ex = ex.InnerException; + } + + return ex; + } + + /// /// Returns the most inner exception. /// From 288d687572c22d8a4fd687104e438c20553a2a3d Mon Sep 17 00:00:00 2001 From: yallie Date: Tue, 10 Dec 2024 16:45:09 +0300 Subject: [PATCH 2/4] Added a unit test for the ToSerializable method. --- CoreRemoting.Tests/ExceptionTests.cs | 20 +++++++++++++++++++ .../Serialization/ExceptionExtensions.cs | 8 +++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/CoreRemoting.Tests/ExceptionTests.cs b/CoreRemoting.Tests/ExceptionTests.cs index 972ac3b..de540ad 100644 --- a/CoreRemoting.Tests/ExceptionTests.cs +++ b/CoreRemoting.Tests/ExceptionTests.cs @@ -32,6 +32,26 @@ public void Exception_can_be_checked_if_it_is_serializable() 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() { diff --git a/CoreRemoting/Serialization/ExceptionExtensions.cs b/CoreRemoting/Serialization/ExceptionExtensions.cs index 93bdecc..07486eb 100644 --- a/CoreRemoting/Serialization/ExceptionExtensions.cs +++ b/CoreRemoting/Serialization/ExceptionExtensions.cs @@ -1,7 +1,7 @@ -using Castle.MicroKernel.ComponentActivator; -using System; +using System; using System.Linq; using System.Reflection; +using Castle.MicroKernel.ComponentActivator; namespace CoreRemoting.Serialization; @@ -22,7 +22,7 @@ public static class ExceptionExtensions agg.InnerException.IsSerializable() && agg.GetType().IsSerializable, - // pesky exception that looks like serializable but really isn't + // this exception is not deserializable ComponentActivatorException cax => false, _ => ex.GetType().IsSerializable && @@ -64,9 +64,7 @@ public static TException CopyDataFrom(this TException ex, Exception public static Exception SkipTargetInvocationExceptions(this Exception ex) { while (ex is TargetInvocationException && ex.InnerException != null) - { ex = ex.InnerException; - } return ex; } From 07096726e5d69bd7b65741e38e7d57a1f594f1e0 Mon Sep 17 00:00:00 2001 From: yallie Date: Tue, 10 Dec 2024 16:46:27 +0300 Subject: [PATCH 3/4] Demonstrate the exception that can't be received by client, issue #105. --- CoreRemoting.Tests/RpcTests.cs | 44 ++++++++++++++++++++++++++++++++ CoreRemoting/ClientRpcContext.cs | 10 ++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/CoreRemoting.Tests/RpcTests.cs b/CoreRemoting.Tests/RpcTests.cs index 8893746..2e44ba7 100644 --- a/CoreRemoting.Tests/RpcTests.cs +++ b/CoreRemoting.Tests/RpcTests.cs @@ -538,6 +538,50 @@ 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(); + + // simulate a database error on the server-side + var proxy = client.CreateProxy(); + var ex = Assert.Throws(() => + proxy.Error("23503: delete from table 'clients' violates foreign key constraint 'order_client_fk' on table 'orders'")) + .GetInnermostException(); + + Assert.NotNull(ex); + Assert.Equal("Deleting clients is not allowed.", ex.Message); + } + finally + { + // reset the error counter for other tests + _serverFixture.ServerErrorCount = 0; + _serverFixture.Server.AfterCall -= AfterCall; + } + } + [Fact] public void Failing_component_constructor_throws_RemoteInvocationException() { diff --git a/CoreRemoting/ClientRpcContext.cs b/CoreRemoting/ClientRpcContext.cs index 5050d15..b70eea6 100644 --- a/CoreRemoting/ClientRpcContext.cs +++ b/CoreRemoting/ClientRpcContext.cs @@ -17,17 +17,17 @@ internal ClientRpcContext() UniqueCallKey = Guid.NewGuid(); WaitHandle = new EventWaitHandle(initialState: false, EventResetMode.ManualReset); } - + /// /// Gets the unique key of RPC call. /// public Guid UniqueCallKey { get; } - + /// /// Gets or sets the result message, that was received from server after the call was invoked on server side. /// public MethodCallResultMessage ResultMessage { get; set; } - + /// /// Gets or sets whether this RPC call is in error state. /// @@ -37,12 +37,12 @@ internal ClientRpcContext() /// Gets or sets an exception that describes an error that occurred on server side RPC invocation. /// public RemoteInvocationException RemoteException { get; set; } - + /// /// Gets a wait handle that is set, when the response of this RPC call is received from server. /// public EventWaitHandle WaitHandle { get; } // TODO: replace with a Task? - + /// /// Frees managed resources. /// From 147e25e70e6c23efe39516dac0ca4fbfbfb0cb0d Mon Sep 17 00:00:00 2001 From: yallie Date: Tue, 10 Dec 2024 17:09:12 +0300 Subject: [PATCH 4/4] Fixed the exception deserialization issue, close #105. --- CoreRemoting.Tests/RpcTests.cs | 9 +- CoreRemoting/ClientRpcContext.cs | 2 +- CoreRemoting/RemotingClient.cs | 171 ++++++++++++++++--------------- 3 files changed, 94 insertions(+), 88 deletions(-) diff --git a/CoreRemoting.Tests/RpcTests.cs b/CoreRemoting.Tests/RpcTests.cs index 2e44ba7..96ecd1f 100644 --- a/CoreRemoting.Tests/RpcTests.cs +++ b/CoreRemoting.Tests/RpcTests.cs @@ -565,14 +565,17 @@ void AfterCall(object sender, ServerRpcContext ctx) 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(); - var ex = Assert.Throws(() => - proxy.Error("23503: delete from table 'clients' violates foreign key constraint 'order_client_fk' on table 'orders'")) - .GetInnermostException(); + var ex = Assert.Throws(() => 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 { diff --git a/CoreRemoting/ClientRpcContext.cs b/CoreRemoting/ClientRpcContext.cs index b70eea6..82d661a 100644 --- a/CoreRemoting/ClientRpcContext.cs +++ b/CoreRemoting/ClientRpcContext.cs @@ -36,7 +36,7 @@ internal ClientRpcContext() /// /// Gets or sets an exception that describes an error that occurred on server side RPC invocation. /// - public RemoteInvocationException RemoteException { get; set; } + public Exception RemoteException { get; set; } /// /// Gets a wait handle that is set, when the response of this RPC call is received from server. diff --git a/CoreRemoting/RemotingClient.cs b/CoreRemoting/RemotingClient.cs index 0b786f8..d996ccd 100644 --- a/CoreRemoting/RemotingClient.cs +++ b/CoreRemoting/RemotingClient.cs @@ -45,20 +45,20 @@ public sealed class RemotingClient : IRemotingClient private byte[] _serverPublicKeyBlob; // ReSharper disable once InconsistentNaming - private static readonly ConcurrentDictionary _clientInstances = + private static readonly ConcurrentDictionary _clientInstances = new ConcurrentDictionary(); - + private static WeakReference _defaultRemotingClientRef; /// /// Event: Fires after client was disconnected. /// public event Action AfterDisconnect; - + #endregion - + #region Construction - + private RemotingClient() { MethodCallMessageBuilder = new MethodCallMessageBuilder(); @@ -71,7 +71,7 @@ private RemotingClient() _authenticationCompletedWaitHandle = new ManualResetEventSlim(initialState: false); _goodbyeCompletedWaitHandle = new ManualResetEventSlim(initialState: false); } - + /// /// Creates a new instance of the RemotingClient class. /// @@ -80,15 +80,15 @@ public RemotingClient(ClientConfig config) : this() { if (config == null) throw new ArgumentException("No config provided and no default configuration found."); - + Serializer = config.Serializer ?? new BsonSerializerAdapter(); MessageEncryption = config.MessageEncryption; - + _config = config; - + if (MessageEncryption) _keyPair = new RsaKeyPair(config.KeySize); - + _channel = config.Channel ?? new TcpClientChannel(); _channel.Init(this); @@ -111,10 +111,10 @@ public RemotingClient(ClientConfig config) : this() oldClient?.Dispose(); return this; }); - - if (!config.IsDefault) + + if (!config.IsDefault) return; - + RemotingClient.DefaultRemotingClient ??= this; } @@ -129,9 +129,9 @@ private void OnDisconnected() activeCalls = _activeCalls; _activeCalls = null; } - + _goodbyeCompletedWaitHandle.Set(); - + foreach (var activeCall in activeCalls) { activeCall.Value.Error = true; @@ -141,14 +141,14 @@ private void OnDisconnected() } #endregion - + #region Properties /// /// Gets the proxy generator instance. /// private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator(); - + /// /// Gets a utility object for building remoting messages. /// @@ -158,7 +158,7 @@ private void OnDisconnected() /// Gets a utility object to provide encryption of remoting messages. /// private IMessageEncryptionManager MessageEncryptionManager { get; } - + /// /// Gets the configured serializer. /// @@ -168,12 +168,12 @@ private void OnDisconnected() /// Gets the local client delegate registry. /// internal ClientDelegateRegistry ClientDelegateRegistry => _delegateRegistry; - + /// /// Gets or sets the invocation timeout in milliseconds. /// public int? InvocationTimeout { get; set; } - + /// /// Gets or sets whether messages should be encrypted or not. /// @@ -208,17 +208,17 @@ public bool HasSession } } - + /// /// Gets the authenticated identity. May be null if authentication failed or if authentication is not configured. /// - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public RemotingIdentity Identity { get; private set; } - + #endregion - + #region Connection management - + /// /// Connects this CoreRemoting client instance to the configured CoreRemoting server. /// @@ -232,7 +232,7 @@ public void Connect() _goodbyeCompletedWaitHandle.Reset(); lock(_syncObject) _activeCalls = new Dictionary(); - + _channel.Connect(); if (_channel.RawMessageTransport.LastException != null) @@ -247,7 +247,7 @@ public void Connect() throw new NetworkException("Handshake with server failed."); else Authenticate(); - + StartKeepSessionAliveTimer(); } @@ -314,7 +314,7 @@ public void Disconnect(bool quiet = false) _handshakeCompletedWaitHandle?.Reset(); _authenticationCompletedWaitHandle?.Reset(); Identity = null; - + AfterDisconnect?.Invoke(); } @@ -325,7 +325,7 @@ private void StartKeepSessionAliveTimer() { if (_config.KeepSessionAliveInterval <= 0) return; - + _keepSessionAliveTimer = new Timer(Convert.ToDouble(_config.KeepSessionAliveInterval * 1000)); @@ -334,7 +334,7 @@ private void StartKeepSessionAliveTimer() } /// - /// Event procedure: Called when the keep session alive timer elapses. + /// Event procedure: Called when the keep session alive timer elapses. /// /// Event sender /// Event arguments @@ -342,10 +342,10 @@ private void KeepSessionAliveTimerOnElapsed(object sender, ElapsedEventArgs e) { if (_keepSessionAliveTimer == null) return; - + if (!_keepSessionAliveTimer.Enabled) return; - + if (_rawMessageTransport == null) return; @@ -354,7 +354,7 @@ private void KeepSessionAliveTimerOnElapsed(object sender, ElapsedEventArgs e) OnDisconnected(); return; } - + // Send empty message to keep session alive _rawMessageTransport.SendMessage(new byte[0]); } @@ -375,7 +375,7 @@ private byte[] SharedSecret() } #endregion - + #region Authentication /// @@ -386,7 +386,7 @@ private void Authenticate() { if (_config.Credentials == null || (_config.Credentials!=null && _config.Credentials.Length == 0)) return; - + if (_authenticationCompletedWaitHandle.IsSet) return; @@ -409,14 +409,14 @@ private void Authenticate() byte[] rawData = Serializer.Serialize(wireMessage); _rawMessageTransport.LastException = null; - + _rawMessageTransport.SendMessage(rawData); - + if (_rawMessageTransport.LastException != null) throw _rawMessageTransport.LastException; _authenticationCompletedWaitHandle.Wait(_config.AuthenticationTimeout * 1000); - + if (!_authenticationCompletedWaitHandle.IsSet) throw new SecurityException("Authentication timeout."); @@ -425,9 +425,9 @@ private void Authenticate() } #endregion - + #region Handling received messages - + /// /// Called when a message is received from server. /// @@ -490,12 +490,12 @@ private void ProcessCompleteHandshakeMessage(WireMessage message) { if (MessageEncryption) { - var signedMessageData = + var signedMessageData = Serializer.Deserialize(message.Data); - - var encryptedSecret = + + var encryptedSecret = Serializer.Deserialize(signedMessageData.MessageRawData); - + _serverPublicKeyBlob = encryptedSecret.SendersPublicKeyBlob; if (!RsaSignature.VerifySignature( @@ -532,7 +532,7 @@ private void ProcessCompleteHandshakeMessage(WireMessage message) private void ProcessAuthenticationResponseMessage(WireMessage message) { byte[] sharedSecret = SharedSecret(); - + var authResponseMessage = Serializer .Deserialize( @@ -546,7 +546,7 @@ private void ProcessAuthenticationResponseMessage(WireMessage message) _isAuthenticated = authResponseMessage.IsAuthenticated; Identity = _isAuthenticated ? authResponseMessage.AuthenticatedIdentity : null; - + _authenticationCompletedWaitHandle.Set(); } @@ -557,7 +557,7 @@ private void ProcessAuthenticationResponseMessage(WireMessage message) private void ProcessRemoteDelegateInvocationMessage(WireMessage message) { byte[] sharedSecret = SharedSecret(); - + var delegateInvocationMessage = Serializer .Deserialize( @@ -567,14 +567,14 @@ private void ProcessRemoteDelegateInvocationMessage(WireMessage message) sharedSecret: sharedSecret, sendersPublicKeyBlob: _serverPublicKeyBlob, sendersPublicKeySize: _keyPair?.KeySize ?? 0)); - + var localDelegate = _delegateRegistry.GetDelegateByHandlerKey(delegateInvocationMessage.HandlerKey); // Invoke local delegate with arguments from remote caller localDelegate.DynamicInvoke(delegateInvocationMessage.DelegateArguments); } - + /// /// Processes a RPC result message from server. /// @@ -584,13 +584,13 @@ private void ProcessRpcResultMessage(WireMessage message) { byte[] sharedSecret = SharedSecret(); - Guid unqiueCallKey = + Guid unqiueCallKey = message.UniqueCallKey == null - ? Guid.Empty + ? Guid.Empty : new Guid(message.UniqueCallKey); ClientRpcContext clientRpcContext; - + lock (_syncObject) { if (_activeCalls == null) @@ -611,7 +611,7 @@ private void ProcessRpcResultMessage(WireMessage message) try { var remoteException = - Serializer.Deserialize( + Serializer.Deserialize( MessageEncryptionManager.GetDecryptedMessageData( message: message, serializer: Serializer, @@ -641,7 +641,7 @@ private void ProcessRpcResultMessage(WireMessage message) sharedSecret: sharedSecret, sendersPublicKeyBlob: _serverPublicKeyBlob, sendersPublicKeySize: _keyPair?.KeySize ?? 0); - + var resultMessage = Serializer .Deserialize(rawMessage); @@ -651,18 +651,21 @@ private void ProcessRpcResultMessage(WireMessage message) catch (Exception e) { clientRpcContext.Error = true; - clientRpcContext.RemoteException = new RemoteInvocationException( - message: e.Message, - innerEx: e.GetType().IsSerializable ? e : null); + + clientRpcContext.RemoteException = + new RemoteInvocationException( + message: e.Message, + innerEx: e.ToSerializable()); } } + clientRpcContext.WaitHandle.Set(); } - + #endregion - + #region RPC - + /// /// Calls a method on a remote service. /// @@ -675,15 +678,15 @@ internal Task InvokeRemoteMethod(MethodCallMessage methodCallM Task.Run(() => { byte[] sharedSecret = SharedSecret(); - + lock (_syncObject) { if (_activeCalls == null) throw new RemoteInvocationException("ServerDisconnected"); } - + var clientRpcContext = new ClientRpcContext(); - + lock (_syncObject) { if (_activeCalls.ContainsKey(clientRpcContext.UniqueCallKey)) @@ -718,7 +721,7 @@ internal Task InvokeRemoteMethod(MethodCallMessage methodCallM throw _rawMessageTransport.LastException; } - if (oneWay || clientRpcContext.ResultMessage != null) + if (oneWay || clientRpcContext.ResultMessage != null) return clientRpcContext; // TODO: replace with a Task to avoid freezing the current thread? @@ -729,14 +732,14 @@ internal Task InvokeRemoteMethod(MethodCallMessage methodCallM return clientRpcContext; }); - + return sendTask; } - + #endregion - + #region Proxy management - + /// /// Creates a proxy object to provide access to a remote service. /// @@ -762,7 +765,7 @@ public object CreateProxy(Type serviceInterfaceType, string serviceName = "") { var serviceProxyType = typeof(ServiceProxy<>).MakeGenericType(serviceInterfaceType); var serviceProxy = Activator.CreateInstance(serviceProxyType, this, serviceName); - + return ProxyGenerator.CreateInterfaceProxyWithoutTarget( interfaceToProxy: serviceInterfaceType, interceptor: (IInterceptor)serviceProxy); @@ -787,16 +790,16 @@ public void ShutdownProxy(object serviceProxy) { if (!ProxyUtil.IsProxy(serviceProxy)) return; - + var proxyType = serviceProxy.GetType(); - - var hiddenInterceptorsField = - proxyType.GetField("__interceptors", + + var hiddenInterceptorsField = + proxyType.GetField("__interceptors", BindingFlags.Instance | BindingFlags.NonPublic); if (hiddenInterceptorsField == null) return; - + var interceptors = hiddenInterceptorsField.GetValue(serviceProxy) as IInterceptor[]; var coreRemotingInterceptor = @@ -808,9 +811,9 @@ where interceptor is IServiceProxy } #endregion - + #region IDisposable implementation - + /// /// Frees managed resources. /// @@ -820,9 +823,9 @@ public void Dispose() RemotingClient.DefaultRemotingClient = null; _clientInstances.TryRemove(_config.UniqueClientInstanceName, out _); - + Disconnect(); - + _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); _delegateRegistry.Clear(); @@ -862,9 +865,9 @@ public void Dispose() _keyPair?.Dispose(); } - + #endregion - + #region Managing client instances /// @@ -900,13 +903,13 @@ public static IRemotingClient DefaultRemotingClient } internal set { - _defaultRemotingClientRef = - value == null - ? null + _defaultRemotingClientRef = + value == null + ? null : new WeakReference(value); } } - + #endregion } }