Skip to content

Commit

Permalink
Handle remote event subscriptions and unsubscriptions to enable event…
Browse files Browse the repository at this point in the history
…s for non-singleton server components.
  • Loading branch information
yallie committed Feb 13, 2025
1 parent 2caaa9e commit a4589f1
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 23 deletions.
23 changes: 23 additions & 0 deletions CoreRemoting.Tests/EventStubTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using CoreRemoting.RemoteDelegates;
using CoreRemoting.Toolbox;
using Xunit;

namespace CoreRemoting.Tests;
Expand Down Expand Up @@ -319,4 +320,26 @@ public void EventStub_Handler_Count_Tests()
eventStub.RemoveHandler(nameof(ISampleInterface.SimpleEvent), handler);
Assert.Equal(1, sampleService.SimpleEventHandlerCount);
}

[Fact]
public void MethodInfo_can_represent_subscription_or_unsubscription()
{
var method = typeof(ISampleInterface).GetMethod("add_SimpleEvent");
Assert.NotNull(method);
Assert.True(method.IsEventAccessor(out var eventName, out var subscription));
Assert.Equal(nameof(ISampleInterface.SimpleEvent), eventName);
Assert.True(subscription);

method = typeof(ISampleInterface).GetMethod("remove_CancelEvent");
Assert.NotNull(method);
Assert.True(method.IsEventAccessor(out eventName, out subscription));
Assert.Equal(nameof(ISampleInterface.CancelEvent), eventName);
Assert.False(subscription);

method = typeof(ISampleInterface).GetMethod(nameof(ISampleInterface.FireHandlers));
Assert.NotNull(method);
Assert.False(method.IsEventAccessor(out eventName, out subscription));
Assert.Null(eventName);
Assert.False(subscription);
}
}
8 changes: 5 additions & 3 deletions CoreRemoting.Tests/RpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,13 @@ void ClientAction()
[Fact]
public async Task Call_on_Proxy_should_be_executed_asynchronously()
{
bool longRunnigCalled = false;
var longRunnigCalled = false;
void BeforeCall(object sender, ServerRpcContext e)
{
if (e.MethodCallMessage.MethodName == "LongRunnigTestMethod")
longRunnigCalled = true;
}

try
{
_serverFixture.Server.BeforeCall += BeforeCall;
Expand Down Expand Up @@ -274,7 +275,8 @@ await Task.Run(() =>
}
finally
{
_serverFixture.Server.BeforeCall += BeforeCall;
_serverFixture.Server.BeforeCall -= BeforeCall;
_serverFixture.ServerErrorCount = 0;
}
}

Expand Down Expand Up @@ -536,7 +538,7 @@ public void Missing_service_throws_RemoteInvocationException()
client.Connect();

var proxy = client.CreateProxy<IDisposable>();
var ex = Assert.Throws<RemoteInvocationException>(() => proxy.Dispose());
var ex = Assert.Throws<RemoteInvocationException>(proxy.Dispose);

// a localized message similar to "Service 'System.IDisposable' is not registered"
Assert.NotNull(ex);
Expand Down
8 changes: 6 additions & 2 deletions CoreRemoting.Tests/Tools/DryIocContainerAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ protected override void RegisterServiceInContainer<TServiceInterface, TServiceIm
protected override void RegisterServiceInContainer<TServiceInterface>(Func<TServiceInterface> factoryDelegate, ServiceLifetime lifetime, string serviceName = "") =>
RootContainer.RegisterDelegate(factoryDelegate, GetReuse(lifetime), serviceKey: GetKey<TServiceInterface>(serviceName));

protected override object ResolveServiceFromContainer(ServiceRegistration registration) =>
Container.Resolve(registration.InterfaceType ?? registration.ImplementationType,
protected override object ResolveServiceFromContainer(ServiceRegistration registration)
{
var service = Container.Resolve(registration.InterfaceType ?? registration.ImplementationType,
serviceKey: GetKey(registration.InterfaceType ?? registration.ImplementationType, registration.ServiceName));
registration.EventStub.WireTo(service);
return service;
}

protected override TServiceInterface ResolveServiceFromContainer<TServiceInterface>(ServiceRegistration registration) =>
ResolveServiceFromContainer(registration) as TServiceInterface;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public CastleWindsorDependencyInjectionContainer()
/// <returns>Resolved service instance</returns>
protected override object ResolveServiceFromContainer(ServiceRegistration registration)
{
return _container.Resolve(key: registration.ServiceName, service: registration.InterfaceType);
var service = _container.Resolve(key: registration.ServiceName, service: registration.InterfaceType);
registration.EventStub.WireTo(service);
return service;
}

/// <summary>
Expand All @@ -39,7 +41,9 @@ protected override object ResolveServiceFromContainer(ServiceRegistration regist
/// <returns>Service instance</returns>
protected override TServiceInterface ResolveServiceFromContainer<TServiceInterface>(ServiceRegistration registration)
{
return _container.Resolve<TServiceInterface>(key: registration.ServiceName);
var service = _container.Resolve<TServiceInterface>(key: registration.ServiceName);
registration.EventStub.WireTo(service);
return service;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ private static void ThrowExceptionIfCustomServiceName(string serviceName, Type s
protected override object ResolveServiceFromContainer(ServiceRegistration registration)
{
ThrowExceptionIfCustomServiceName(registration.ServiceName, registration.InterfaceType);
return _serviceProvider.GetRequiredService(registration.InterfaceType);
var service = _serviceProvider.GetRequiredService(registration.InterfaceType);
registration.EventStub.WireTo(service);
return service;
}

/// <summary>
Expand All @@ -56,7 +58,9 @@ protected override TServiceInterface ResolveServiceFromContainer<TServiceInterfa
Type serviceInterfaceType = typeof(TServiceInterface);
ThrowExceptionIfCustomServiceName(registration.ServiceName, serviceInterfaceType);

return _serviceProvider.GetRequiredService<TServiceInterface>();
var service = _serviceProvider.GetRequiredService<TServiceInterface>();
registration.EventStub.WireTo(service);
return service;
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions CoreRemoting/DependencyInjection/ServiceRegistration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using CoreRemoting.RemoteDelegates;

namespace CoreRemoting.DependencyInjection;

Expand Down Expand Up @@ -38,6 +39,7 @@ public ServiceRegistration(
ServiceLifetime = serviceLifetime;
Factory = factory;
IsHiddenSystemService = isHiddenSystemService;
EventStub = new EventStub(interfaceType);
}

/// <summary>
Expand Down Expand Up @@ -74,4 +76,9 @@ public ServiceRegistration(
/// Returns whether the registered service is a hidden system service or not.
/// </summary>
public bool IsHiddenSystemService { get; }

/// <summary>
/// Holds all remote event handlers for the component.
/// </summary>
public EventStub EventStub { get; }
}
20 changes: 10 additions & 10 deletions CoreRemoting/RemoteDelegates/EventStub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,9 @@ public int HandlerCount
/// <param name="instance">The instance.</param>
public void WireTo(object instance)
{
if (instance == null)
if (instance == null ||
EventProperties.Length +
DelegateProperties.Length == 0)
{
return;
}
Expand All @@ -325,13 +327,11 @@ public void WireTo(object instance)
eventInfo.AddEventHandler(instance, this[eventInfo.Name]);
}

var indexes = new object[0];

foreach (var propInfo in DelegateProperties)
{
var value = propInfo.GetValue(instance, indexes) as Delegate;
var value = propInfo.GetValue(instance, []) as Delegate;
value = Delegate.Combine(value, this[propInfo.Name]);
propInfo.SetValue(instance, value, indexes);
propInfo.SetValue(instance, value, []);
}
}

Expand All @@ -341,7 +341,9 @@ public void WireTo(object instance)
/// <param name="instance">The instance.</param>
public void UnwireFrom(object instance)
{
if (instance == null)
if (instance == null ||
EventProperties.Length +
DelegateProperties.Length == 0)
{
return;
}
Expand All @@ -351,13 +353,11 @@ public void UnwireFrom(object instance)
eventInfo.RemoveEventHandler(instance, this[eventInfo.Name]);
}

var indexes = new object[0];

foreach (var propInfo in DelegateProperties)
{
var value = propInfo.GetValue(instance, indexes) as Delegate;
var value = propInfo.GetValue(instance, []) as Delegate;
value = Delegate.Remove(value, this[propInfo.Name]);
propInfo.SetValue(instance, value, indexes);
propInfo.SetValue(instance, value, []);
}
}

Expand Down
40 changes: 36 additions & 4 deletions CoreRemoting/RemotingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,12 @@ private async Task ProcessRpcMessage(WireMessage request)
if (serverRpcContext.AuthenticationRequired && !_isAuthenticated)
throw new NetworkException("Session is not authenticated.");

var registration = _server.ServiceRegistry.GetServiceRegistration(callMessage.ServiceName);
var service = _server.ServiceRegistry.GetService(callMessage.ServiceName);
var serviceInterfaceType =
_server.ServiceRegistry.GetServiceInterfaceType(callMessage.ServiceName);
var serviceInterfaceType = registration.InterfaceType;

serverRpcContext.ServiceInstance = service;
serverRpcContext.EventStub = registration.EventStub;

method = GetMethodInfo(callMessage, serviceInterfaceType, parameterTypes);
if (method == null)
Expand Down Expand Up @@ -501,8 +502,19 @@ private async Task ProcessRpcMessage(WireMessage request)
{
((RemotingServer)_server).OnBeforeCall(serverRpcContext);

result = method.Invoke(serverRpcContext.ServiceInstance,
serverRpcContext.MethodCallParameterValues);
if (method.IsEventAccessor(out var eventName, out var subscription))
{
// event accessor is called
HandleEventSubscription(serverRpcContext.EventStub,
eventName, subscription, serverRpcContext.MethodCallParameterValues);
result = null;
}
else
{
// normal method is called
result = method.Invoke(serverRpcContext.ServiceInstance,
serverRpcContext.MethodCallParameterValues);
}

var returnType = method.ReturnType;

Expand Down Expand Up @@ -604,6 +616,26 @@ await _rawMessageTransport.SendMessageAsync(
.ConfigureAwait(false);
}

private void HandleEventSubscription(EventStub eventStub, string eventName, bool subscription, object[] parameters)
{
if (parameters == null || parameters.Length != 1)
{
return;
}

var eventHandler = parameters[0] as Delegate;
if (eventHandler == null)
{
return;
}

Action<string, Delegate> eventAccessor = subscription ?
eventStub.AddHandler :
eventStub.RemoveHandler;

eventAccessor(eventName, eventHandler);
}

private MethodInfo GetMethodInfo(MethodCallMessage callMessage, Type serviceInterfaceType, Type[] parameterTypes)
{
MethodInfo method;
Expand Down
7 changes: 7 additions & 0 deletions CoreRemoting/ServerRpcContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using CoreRemoting.DependencyInjection;
using CoreRemoting.RemoteDelegates;
using CoreRemoting.RpcMessaging;

namespace CoreRemoting
Expand Down Expand Up @@ -49,6 +51,11 @@ public class ServerRpcContext
/// </summary>
public MethodCallResultMessage MethodCallResultMessage { get; set; }

/// <summary>
/// Gets or sets service event stub.
/// </summary>
public EventStub EventStub { get; set; }

/// <summary>
/// Gets or sets the instance of the service, on which the method is called.
/// </summary>
Expand Down
46 changes: 46 additions & 0 deletions CoreRemoting/Toolbox/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Reflection;

namespace CoreRemoting.Toolbox
{
Expand All @@ -8,6 +9,51 @@ namespace CoreRemoting.Toolbox
/// </summary>
public static class Extensions
{
/// <summary>
/// Checks whether the given method represents event subscription or unsubscription.
/// </summary>
/// <param name="method">Method information.</param>
/// <param name="eventName">If return value is true, this parameter returns the name of the event.</param>
/// <param name="subscription">If true, method represents subscription, otherwise, it's unsubscription.</param>
public static bool IsEventAccessor(this MethodInfo method, out string eventName, out bool subscription)
{
// void add_Click(EventHandler e) → subscription to Click
// void remove_Click(EventHandler e) → unsubscription from Click
eventName = null;
subscription = false;

if (method == null ||
method.IsGenericMethod ||
method.ReturnType != typeof(void))
{
return false;
}

if (method.Name.StartsWith("add_"))
{
eventName = method.Name.Substring(4);
subscription = true;
}
else if (method.Name.StartsWith("remove_"))
{
eventName = method.Name.Substring(7);
subscription = false;
}
else
{
return false;
}

var parameters = method.GetParameters();
if (parameters.Length != 1)
{
return false;
}

return typeof(Delegate)
.IsAssignableFrom(parameters[0].ParameterType);
}

private static ConcurrentDictionary<Type, object> DefaultValues = new();

/// <summary>
Expand Down

0 comments on commit a4589f1

Please sign in to comment.