diff --git a/CoreRemoting.Tests/EventStubTests.cs b/CoreRemoting.Tests/EventStubTests.cs new file mode 100644 index 0000000..b5487a4 --- /dev/null +++ b/CoreRemoting.Tests/EventStubTests.cs @@ -0,0 +1,345 @@ +using System; +using System.ComponentModel; +using CoreRemoting.RemoteDelegates; +using CoreRemoting.Toolbox; +using Xunit; + +namespace CoreRemoting.Tests; + +public class EventStubTests +{ + public interface ISampleInterface + { + string FireHandlers(int argument); + + event EventHandler SimpleEvent; + + event EventHandler CancelEvent; + + Action ActionDelegate { get; set; } + + Func FuncDelegate { get; set; } + + int SimpleEventHandlerCount { get; } + } + + public interface ISampleDescendant1 : ISampleInterface + { + event EventHandler NewEvent; + + Action NewDelegate { get; set; } + } + + public interface ISampleDescendant2 : ISampleDescendant1, ISampleInterface + { + event EventHandler NewCancelEvent; + } + + public class SampleService : ISampleInterface + { + public string FireHandlers(int argument) + { + if (SimpleEvent != null) + { + SimpleEvent(this, EventArgs.Empty); + } + + if (CancelEvent != null) + { + CancelEvent(this, new CancelEventArgs()); + } + + if (ActionDelegate != null) + { + ActionDelegate(); + } + + if (FuncDelegate != null) + { + return FuncDelegate(argument); + } + + return null; + } + + public event EventHandler SimpleEvent; + + public event EventHandler CancelEvent; + + public Action ActionDelegate { get; set; } + + public Func FuncDelegate { get; set; } + + public int SimpleEventHandlerCount + { + get { return EventStub.GetHandlerCount(SimpleEvent); } + } + } + + [Fact] + public void EventStub_Contains_Events_And_Delegates() + { + var eventStub = new EventStub(typeof(ISampleInterface)); + Assert.NotNull(eventStub[nameof(ISampleInterface.SimpleEvent)]); + Assert.NotNull(eventStub[nameof(ISampleInterface.CancelEvent)]); + Assert.NotNull(eventStub[nameof(ISampleInterface.ActionDelegate)]); + Assert.NotNull(eventStub[nameof(ISampleInterface.FuncDelegate)]); + } + + [Fact] + public void EventStub_Contains_Inherited_Events_And_Delegates() + { + var eventStub = new EventStub(typeof(ISampleDescendant2)); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.NewCancelEvent)]); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.NewEvent)]); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.NewDelegate)]); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.SimpleEvent)]); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.CancelEvent)]); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.ActionDelegate)]); + Assert.NotNull(eventStub[nameof(ISampleDescendant2.FuncDelegate)]); + } + + [Fact] + public void EventStub_Delegates_Have_Same_Types_As_Their_Originals() + { + var eventStub = new EventStub(typeof(ISampleInterface)); + Assert.IsType(eventStub[nameof(ISampleInterface.SimpleEvent)]); + Assert.IsType>(eventStub[nameof(ISampleInterface.CancelEvent)]); + Assert.IsType(eventStub[nameof(ISampleInterface.ActionDelegate)]); + Assert.IsType>(eventStub[nameof(ISampleInterface.FuncDelegate)]); + } + + [Fact] + public void EventStub_Simple_Handle_Tests() + { + // add the first handler + var eventStub = new EventStub(typeof(ISampleInterface)); + var fired = false; + eventStub.AddHandler(nameof(ISampleInterface.SimpleEvent), new EventHandler((sender, args) => fired = true)); + + // check if it is called + var handler = (EventHandler)eventStub[nameof(ISampleInterface.SimpleEvent)]; + handler(this, EventArgs.Empty); + Assert.True(fired); + + // add the second handler + fired = false; + var firedAgain = false; + var tempHandler = new EventHandler((sender, args) => firedAgain = true); + eventStub.AddHandler(nameof(ISampleInterface.SimpleEvent), tempHandler); + + // check if it is called + handler(this, EventArgs.Empty); + Assert.True(fired); + Assert.True(firedAgain); + + // remove the second handler + fired = false; + firedAgain = false; + eventStub.RemoveHandler(nameof(ISampleInterface.SimpleEvent), tempHandler); + + // check if it is not called + handler(this, EventArgs.Empty); + Assert.True(fired); + Assert.False(firedAgain); + } + + [Fact] + public void EventStub_Cancel_Event_Tests() + { + // add the first handler + var eventStub = new EventStub(typeof(ISampleInterface)); + var fired = false; + eventStub.AddHandler(nameof(ISampleInterface.CancelEvent), new EventHandler((sender, args) => fired = true)); + + // check if it is called + var handler = (EventHandler)eventStub[nameof(ISampleInterface.CancelEvent)]; + handler(this, new CancelEventArgs()); + Assert.True(fired); + + // add the second handler + fired = false; + var firedAgain = false; + var tempHandler = new EventHandler((sender, args) => firedAgain = true); + eventStub.AddHandler(nameof(ISampleInterface.CancelEvent), tempHandler); + + // check if it is called + handler(this, new CancelEventArgs()); + Assert.True(fired); + Assert.True(firedAgain); + + // remove the second handler + fired = false; + firedAgain = false; + eventStub.RemoveHandler(nameof(ISampleInterface.CancelEvent), tempHandler); + + // check if it is not called + handler(this, new CancelEventArgs()); + Assert.True(fired); + Assert.False(firedAgain); + } + + [Fact] + public void EventStub_ActionDelegateTests() + { + // add the first handler + var eventStub = new EventStub(typeof(ISampleInterface)); + var fired = false; + eventStub.AddHandler(nameof(ISampleInterface.ActionDelegate), new Action(() => fired = true)); + + // check if it is called + var handler = (Action)eventStub[nameof(ISampleInterface.ActionDelegate)]; + handler(); + Assert.True(fired); + + // add the second handler + fired = false; + var firedAgain = false; + var tempHandler = new Action(() => firedAgain = true); + eventStub.AddHandler(nameof(ISampleInterface.ActionDelegate), tempHandler); + + // check if it is called + handler(); + Assert.True(fired); + Assert.True(firedAgain); + + // remove the second handler + fired = false; + firedAgain = false; + eventStub.RemoveHandler(nameof(ISampleInterface.ActionDelegate), tempHandler); + + // check if it is not called + handler(); + Assert.True(fired); + Assert.False(firedAgain); + } + + [Fact] + public void EventStub_FuncDelegateTests() + { + // add the first handler + var eventStub = new EventStub(typeof(ISampleInterface)); + var fired = false; + eventStub.AddHandler(nameof(ISampleInterface.FuncDelegate), new Func(a => + { + fired = true; + return a.ToString(); + })); + + // check if it is called + var handler = (Func)eventStub[nameof(ISampleInterface.FuncDelegate)]; + var result = handler(123); + Assert.True(fired); + Assert.Null(result); // Assert.Equal("123", result); + + // add the second handler + fired = false; + var firedAgain = false; + var tempHandler = new Func(a => { firedAgain = true; return a.ToString(); }); + eventStub.AddHandler(nameof(ISampleInterface.FuncDelegate), tempHandler); + + // check if it is called + result = handler(321); + Assert.True(fired); + Assert.True(firedAgain); + Assert.Null(result); // Assert.Equal("321", result); + + // remove the second handler + fired = false; + firedAgain = false; + eventStub.RemoveHandler(nameof(ISampleInterface.FuncDelegate), tempHandler); + + // check if it is not called + result = handler(0); + Assert.True(fired); + Assert.False(firedAgain); + Assert.Null(result); // Assert.Equal("0", result); + } + + [Fact] + public void EventStub_WireUnwireTests() + { + var eventStub = new EventStub(typeof(ISampleInterface)); + var simpleEventFired = false; + var cancelEventFired = false; + var actionFired = false; + var funcFired = false; + + // add event handlers + eventStub.AddHandler(nameof(ISampleInterface.SimpleEvent), new EventHandler((sender, args) => simpleEventFired = true)); + eventStub.AddHandler(nameof(ISampleInterface.CancelEvent), new EventHandler((sender, args) => cancelEventFired = true)); + eventStub.AddHandler(nameof(ISampleInterface.ActionDelegate), new Action(() => actionFired = true)); + eventStub.AddHandler(nameof(ISampleInterface.FuncDelegate), new Func(a => { funcFired = true; return a.ToString(); })); + eventStub.AddHandler(nameof(ISampleInterface.FuncDelegate), new Func(a => { return (a * 2).ToString(); })); + + // wire up events + var component = new SampleService(); + eventStub.WireTo(component); + + // test if it works + var result = component.FireHandlers(102030); + Assert.Null(result); // Assert.Equal("204060", result); + Assert.True(simpleEventFired); + Assert.True(cancelEventFired); + Assert.True(actionFired); + Assert.True(funcFired); + + // unwire + simpleEventFired = false; + cancelEventFired = false; + actionFired = false; + funcFired = false; + eventStub.UnwireFrom(component); + + // test if it works + result = component.FireHandlers(123); + Assert.Null(result); + Assert.False(simpleEventFired); + Assert.False(cancelEventFired); + Assert.False(actionFired); + Assert.False(funcFired); + } + + [Fact] + public void EventStub_Handler_Count_Tests() + { + var eventStub = new EventStub(typeof(ISampleInterface)); + var sampleService = new SampleService(); + + eventStub.WireTo(sampleService); + Assert.Equal(0, sampleService.SimpleEventHandlerCount); + + eventStub.AddHandler(nameof(ISampleInterface.SimpleEvent), new EventHandler((s, e) => { })); + Assert.Equal(1, sampleService.SimpleEventHandlerCount); + + var handler = new EventHandler((s, e) => { }); + + eventStub.AddHandler(nameof(ISampleInterface.SimpleEvent), handler); + Assert.Equal(2, sampleService.SimpleEventHandlerCount); + + 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); + } +} diff --git a/CoreRemoting.Tests/RpcTests.cs b/CoreRemoting.Tests/RpcTests.cs index 556322d..34facba 100644 --- a/CoreRemoting.Tests/RpcTests.cs +++ b/CoreRemoting.Tests/RpcTests.cs @@ -234,16 +234,17 @@ void ClientAction() Assert.Equal("test", argumentFromServer); Assert.Equal(0, _serverFixture.ServerErrorCount); } - + [Fact] - public async void Call_on_Proxy_should_be_executed_asynchronously() + 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; @@ -274,12 +275,51 @@ await Task.Run(() => } finally { - _serverFixture.Server.BeforeCall += BeforeCall; + _serverFixture.Server.BeforeCall -= BeforeCall; + _serverFixture.ServerErrorCount = 0; } } - [Fact] - public void Events_should_work_remotely() + [Theory] + [InlineData("TestService_Singleton_Service")] + [InlineData("TestService_Singleton_Factory")] + [InlineData("TestService_SingleCall_Service")] + [InlineData("TestService_SingleCall_Factory")] + [InlineData("TestService_Scoped_Service")] + [InlineData("TestService_Scoped_Factory")] + public void Component_lifetime_matches_the_expectation(string serviceName) + { + using var ctx = ValidationSyncContext.Install(); + using var client = new RemotingClient( + new ClientConfig() + { + ConnectionTimeout = 0, + SendTimeout = 0, + Channel = ClientChannel, + MessageEncryption = false, + ServerPort = _serverFixture.Server.Config.NetworkPort, + }); + + client.Connect(); + + var proxy = client.CreateProxy(serviceName); + + // check if service lifetime matches the expectations + proxy.SaveLastInstance(); + + // singleton component should have the same instance + var sameInstance = serviceName.Contains("Singleton"); + Assert.Equal(sameInstance, proxy.CheckLastSavedInstance()); + } + + [Theory] + [InlineData("TestService_Singleton_Service")] + [InlineData("TestService_Singleton_Factory")] + [InlineData("TestService_SingleCall_Service")] + [InlineData("TestService_SingleCall_Factory")] + [InlineData("TestService_Scoped_Service")] + [InlineData("TestService_Scoped_Factory")] + public void Events_should_work_remotely(string serviceName) { using var ctx = ValidationSyncContext.Install(); @@ -298,7 +338,7 @@ public void Events_should_work_remotely() client.Connect(); - var proxy = client.CreateProxy(); + var proxy = client.CreateProxy(serviceName); var serviceEventResetEvent = new ManualResetEventSlim(initialState: false); var customDelegateEventResetEvent = new ManualResetEventSlim(initialState: false); @@ -498,7 +538,7 @@ public void Missing_service_throws_RemoteInvocationException() client.Connect(); var proxy = client.CreateProxy(); - var ex = Assert.Throws(() => proxy.Dispose()); + var ex = Assert.Throws(proxy.Dispose); // a localized message similar to "Service 'System.IDisposable' is not registered" Assert.NotNull(ex); diff --git a/CoreRemoting.Tests/ServerFixture.cs b/CoreRemoting.Tests/ServerFixture.cs index aa1374a..18cf117 100644 --- a/CoreRemoting.Tests/ServerFixture.cs +++ b/CoreRemoting.Tests/ServerFixture.cs @@ -29,6 +29,29 @@ public ServerFixture() factoryDelegate: () => TestService, lifetime: ServiceLifetime.Singleton); + // Services for event tests + container.RegisterService( + lifetime: ServiceLifetime.Singleton, + serviceName: "TestService_Singleton_Service"); + container.RegisterService( + lifetime: ServiceLifetime.SingleCall, + serviceName: "TestService_SingleCall_Service"); + container.RegisterService( + lifetime: ServiceLifetime.Scoped, + serviceName: "TestService_Scoped_Service"); + container.RegisterService( + factoryDelegate: () => new TestService(), + lifetime: ServiceLifetime.Singleton, + serviceName: "TestService_Singleton_Factory"); + container.RegisterService( + factoryDelegate: () => new TestService(), + lifetime: ServiceLifetime.SingleCall, + serviceName: "TestService_SingleCall_Factory"); + container.RegisterService( + factoryDelegate: () => new TestService(), + lifetime: ServiceLifetime.Scoped, + serviceName: "TestService_Scoped_Factory"); + // Service for async tests container.RegisterService( lifetime: ServiceLifetime.Singleton); diff --git a/CoreRemoting.Tests/Tools/DryIocContainerAdapter.cs b/CoreRemoting.Tests/Tools/DryIocContainerAdapter.cs index be13f5e..be16ac4 100644 --- a/CoreRemoting.Tests/Tools/DryIocContainerAdapter.cs +++ b/CoreRemoting.Tests/Tools/DryIocContainerAdapter.cs @@ -59,9 +59,13 @@ protected override void RegisterServiceInContainer(Func factoryDelegate, ServiceLifetime lifetime, string serviceName = "") => RootContainer.RegisterDelegate(factoryDelegate, GetReuse(lifetime), serviceKey: GetKey(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(ServiceRegistration registration) => ResolveServiceFromContainer(registration) as TServiceInterface; diff --git a/CoreRemoting.Tests/Tools/ITestService.cs b/CoreRemoting.Tests/Tools/ITestService.cs index 9c4f7ad..fe7a9ca 100644 --- a/CoreRemoting.Tests/Tools/ITestService.cs +++ b/CoreRemoting.Tests/Tools/ITestService.cs @@ -13,7 +13,7 @@ public interface ITestService : IBaseService event Action ServiceEvent; event ServerEventHandler CustomDelegateEvent; - + object TestMethod(object arg); object LongRunnigTestMethod(int timeout); @@ -44,4 +44,8 @@ public interface ITestService : IBaseService DataTable TestDt(DataTable dt, long num); (T duplicate, int size) Duplicate(T sample) where T : class; + + void SaveLastInstance(); + + bool CheckLastSavedInstance(); } \ No newline at end of file diff --git a/CoreRemoting.Tests/Tools/TestService.cs b/CoreRemoting.Tests/Tools/TestService.cs index 0b5748b..9cd8f4c 100644 --- a/CoreRemoting.Tests/Tools/TestService.cs +++ b/CoreRemoting.Tests/Tools/TestService.cs @@ -9,8 +9,9 @@ namespace CoreRemoting.Tests.Tools; public class TestService : ITestService { + private int _counter; - + public Func TestMethodFake { get; set; } public Action OneWayMethodFake { get; set; } @@ -131,4 +132,11 @@ TItem[] Dup(TItem[] arr) return arr; } } + + private static TestService LastInstance { get; set; } + + public void SaveLastInstance() => LastInstance = this; + + public bool CheckLastSavedInstance() => LastInstance == this; + } \ No newline at end of file diff --git a/CoreRemoting/DependencyInjection/CastleWindsorDependencyInjectionContainer.cs b/CoreRemoting/DependencyInjection/CastleWindsorDependencyInjectionContainer.cs index 8772770..ffff926 100644 --- a/CoreRemoting/DependencyInjection/CastleWindsorDependencyInjectionContainer.cs +++ b/CoreRemoting/DependencyInjection/CastleWindsorDependencyInjectionContainer.cs @@ -28,7 +28,9 @@ public CastleWindsorDependencyInjectionContainer() /// Resolved service instance 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; } /// @@ -39,7 +41,9 @@ protected override object ResolveServiceFromContainer(ServiceRegistration regist /// Service instance protected override TServiceInterface ResolveServiceFromContainer(ServiceRegistration registration) { - return _container.Resolve(key: registration.ServiceName); + var service = _container.Resolve(key: registration.ServiceName); + registration.EventStub.WireTo(service); + return service; } /// diff --git a/CoreRemoting/DependencyInjection/MicrosoftDependencyInjectionContainer.cs b/CoreRemoting/DependencyInjection/MicrosoftDependencyInjectionContainer.cs index 34f2c6e..13eedc6 100644 --- a/CoreRemoting/DependencyInjection/MicrosoftDependencyInjectionContainer.cs +++ b/CoreRemoting/DependencyInjection/MicrosoftDependencyInjectionContainer.cs @@ -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; } /// @@ -56,7 +58,9 @@ protected override TServiceInterface ResolveServiceFromContainer(); + var service = _serviceProvider.GetRequiredService(); + registration.EventStub.WireTo(service); + return service; } /// diff --git a/CoreRemoting/DependencyInjection/ServiceRegistration.cs b/CoreRemoting/DependencyInjection/ServiceRegistration.cs index 0abad74..34d9481 100644 --- a/CoreRemoting/DependencyInjection/ServiceRegistration.cs +++ b/CoreRemoting/DependencyInjection/ServiceRegistration.cs @@ -1,4 +1,5 @@ using System; +using CoreRemoting.RemoteDelegates; namespace CoreRemoting.DependencyInjection; @@ -38,6 +39,7 @@ public ServiceRegistration( ServiceLifetime = serviceLifetime; Factory = factory; IsHiddenSystemService = isHiddenSystemService; + EventStub = new EventStub(interfaceType); } /// @@ -74,4 +76,9 @@ public ServiceRegistration( /// Returns whether the registered service is a hidden system service or not. /// public bool IsHiddenSystemService { get; } + + /// + /// Holds all remote event handlers for the component. + /// + public EventStub EventStub { get; } } \ No newline at end of file diff --git a/CoreRemoting/RemoteDelegates/EventStub.cs b/CoreRemoting/RemoteDelegates/EventStub.cs new file mode 100644 index 0000000..f0d5653 --- /dev/null +++ b/CoreRemoting/RemoteDelegates/EventStub.cs @@ -0,0 +1,412 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using CoreRemoting.Toolbox; + +namespace CoreRemoting.RemoteDelegates; + +/// +/// Event stub caches all event handlers for the single-call or scoped components. +/// +public class EventStub +{ + /// + /// Initializes a new instance of the class. + /// + /// Type of the interface. + public EventStub(Type interfaceType) + { + InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType)); + CreateDelegateHolders(); + } + + /// + /// Gets or sets the invocation delegates for event handlers. + /// + private ConcurrentDictionary DelegateHolders { get; set; } + + /// + /// Gets the type of the interface. + /// + public Type InterfaceType { get; private set; } + + /// + /// Gets or sets the with the specified event property name. + /// + /// Name of the event or delegate property. + public Delegate this[string propertyName] => DelegateHolders[propertyName].InvocationDelegate; + + /// + /// Gets or sets the list of event of the reflected interface. + /// + private EventInfo[] EventProperties { get; set; } + + /// + /// Gets or sets the list of delegate properties of the reflected interface. + /// + private PropertyInfo[] DelegateProperties { get; set; } + + private void CreateDelegateHolders() + { + var bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy; + DelegateHolders = new ConcurrentDictionary(); + EventProperties = GetEvents(InterfaceType, bindingFlags).ToArray(); + DelegateProperties = GetDelegateProperties(InterfaceType, bindingFlags).ToArray(); + + foreach (var eventProperty in EventProperties) + { + DelegateHolders[eventProperty.Name] = CreateDelegateHolder(eventProperty.EventHandlerType); + } + + foreach (var delegateProperty in DelegateProperties) + { + DelegateHolders[delegateProperty.Name] = CreateDelegateHolder(delegateProperty.PropertyType); + } + } + + private IEnumerable GetAllInterfaces(Type interfaceType) + { + if (interfaceType.IsInterface) + { + yield return interfaceType; + } + + // Passing BindingFlags.FlattenHierarchy to one of the Type.GetXXX methods, such as Type.GetMembers, + // will not return inherited interface members when you are querying on an interface type itself. + // To get the inherited members, you need to query each implemented interface for its members. + var inheritedInterfaces = + from inheritedInterface in interfaceType.GetInterfaces() + from type in GetAllInterfaces(inheritedInterface) + select type; + + foreach (var type in inheritedInterfaces) + { + yield return type; + } + } + + private IEnumerable GetEvents(Type interfaceType, BindingFlags flags) => + from type in GetAllInterfaces(interfaceType) + from ev in type.GetEvents(flags) + select ev; + + private IEnumerable GetDelegateProperties(Type interfaceType, BindingFlags flags) => + from type in GetAllInterfaces(interfaceType) + from prop in type.GetProperties(flags) + where typeof(Delegate).IsAssignableFrom(prop.PropertyType) + select prop; + + private static IDelegateHolder CreateDelegateHolder(Type delegateType) + { + var createDelegateHolder = createDelegateHolderMethod + .MakeGenericMethod(delegateType) + .CreateDelegate(typeof(Func)) as Func; + + return createDelegateHolder(); + } + + private static MethodInfo createDelegateHolderMethod = + new Func(CreateDelegateHolder).Method.GetGenericMethodDefinition(); + + private static IDelegateHolder CreateDelegateHolder() => + new DelegateHolder(); + + /// + /// Non-generic interface for the private generic delegate holder class. + /// + public interface IDelegateHolder + { + /// + /// Gets the invocation delegate. + /// + Delegate InvocationDelegate { get; } + + /// + /// Adds the handler. + /// + /// The handler. + void AddHandler(Delegate handler); + + /// + /// Removes the handler. + /// + /// The handler. + void RemoveHandler(Delegate handler); + + /// + /// Gets the handler count. + /// + int HandlerCount { get; } + } + + /// + /// Generic holder for delegates (such as event handlers). + /// + private class DelegateHolder : IDelegateHolder + { + public DelegateHolder() + { + // create default return value for the delegate + DefaultReturnValue = typeof(T).GetMethod("Invoke").ReturnType.GetDefaultValue(); + } + + public Delegate InvocationDelegate => + (Delegate)(object)InvocationMethod; + + private T invocationMethod; + + public T InvocationMethod + { + get + { + if (invocationMethod == null) + { + // create strong-typed Invoke method that calls into DynamicInvoke + var dynamicInvokeMethod = GetType().GetMethod(nameof(DynamicInvoke), BindingFlags.NonPublic | BindingFlags.Instance); + invocationMethod = BuildInstanceDelegate(dynamicInvokeMethod, this); + } + + return invocationMethod; + } + } + + /// + /// Builds the strong-typed delegate bound to the given target instance + /// for the dynamicInvoke method: object DynamicInvoke(object[] args); + /// + /// + /// Relies on the dynamic methods. Delegate Target property is equal to the "target" parameter. + /// Doesn't support static methods. + /// + /// Delegate type. + /// for the DynamicInvoke(object[] args) method. + /// Target instance. + /// Strong-typed delegate. + public static TDelegate BuildInstanceDelegate(MethodInfo dynamicInvoke, object target) + { + // validate generic argument + if (!typeof(Delegate).IsAssignableFrom(typeof(TDelegate))) + { + throw new ApplicationException(typeof(TDelegate).FullName + " is not a delegate type."); + } + + // reflect delegate type to get parameters and method return type + var delegateType = typeof(TDelegate); + var invokeMethod = delegateType.GetMethod("Invoke"); + + // figure out parameters + var paramTypeList = invokeMethod.GetParameters().Select(p => p.ParameterType).ToList(); + var paramCount = paramTypeList.Count; + var ownerType = target.GetType(); + paramTypeList.Insert(0, ownerType); + var paramTypes = paramTypeList.ToArray(); + var typedInvoke = new DynamicMethod("TypedInvoke", invokeMethod.ReturnType, paramTypes, ownerType); + + // create method body, declare local variable of type object[] + var ilGenerator = typedInvoke.GetILGenerator(); + var argumentsArray = ilGenerator.DeclareLocal(typeof(object[])); + + // var args = new object[paramCount]; + ilGenerator.Emit(OpCodes.Nop); + ilGenerator.Emit(OpCodes.Ldc_I4, paramCount); + ilGenerator.Emit(OpCodes.Newarr, typeof(object)); + ilGenerator.Emit(OpCodes.Stloc, argumentsArray); + + // load method arguments one by one + var index = 1; + foreach (var paramType in paramTypes.Skip(1)) + { + // load object[] array reference + ilGenerator.Emit(OpCodes.Ldloc, argumentsArray); + ilGenerator.Emit(OpCodes.Ldc_I4, index - 1); // array index + ilGenerator.Emit(OpCodes.Ldarg, index++); // method parameter index + + // value type parameters need boxing + if (typeof(ValueType).IsAssignableFrom(paramType)) + { + ilGenerator.Emit(OpCodes.Box, paramType); + } + + // store reference + ilGenerator.Emit(OpCodes.Stelem_Ref); + } + + // this + ilGenerator.Emit(OpCodes.Ldarg_0); + ilGenerator.Emit(OpCodes.Ldloc, argumentsArray); // object[] args + ilGenerator.Emit(OpCodes.Call, dynamicInvoke); + + // discard return value + if (invokeMethod.ReturnType == typeof(void)) + { + ilGenerator.Emit(OpCodes.Pop); + } + + // unbox return value of value type + else if (typeof(ValueType).IsAssignableFrom(invokeMethod.ReturnType)) + { + ilGenerator.Emit(OpCodes.Unbox_Any, invokeMethod.ReturnType); + } + + // return value + ilGenerator.Emit(OpCodes.Ret); + + // bake dynamic method, create a gelegate + var result = typedInvoke.CreateDelegate(delegateType, target); + return (TDelegate)(object)result; + } + + + private object DynamicInvoke(object[] arguments) + { + // TODO: run in non-blocking mode + Delegate.DynamicInvoke(arguments); + return DefaultReturnValue; + } + + private T TypedDelegate { get; set; } + + private object DefaultReturnValue { get; set; } + + private Delegate Delegate + { + get { return (Delegate)(object)TypedDelegate; } + set { TypedDelegate = (T)(object)value; } + } + + private object syncRoot = new object(); + + public void AddHandler(Delegate handler) + { + lock (syncRoot) + { + Delegate = Delegate.Combine(Delegate, handler); + } + } + + public void RemoveHandler(Delegate handler) + { + lock (syncRoot) + { + Delegate = Delegate.Remove(Delegate, handler); + } + } + + public int HandlerCount + { + get + { + if (Delegate == null) + { + return 0; + } + + return Delegate.GetInvocationList().Length; + } + } + } + + /// + /// Wires all event handlers to the specified instance. + /// + /// The instance. + public void WireTo(object instance) + { + if (instance == null || + EventProperties.Length + + DelegateProperties.Length == 0) + { + return; + } + + foreach (var eventInfo in EventProperties) + { + eventInfo.AddEventHandler(instance, this[eventInfo.Name]); + } + + foreach (var propInfo in DelegateProperties) + { + var value = propInfo.GetValue(instance, []) as Delegate; + value = Delegate.Combine(value, this[propInfo.Name]); + propInfo.SetValue(instance, value, []); + } + } + + /// + /// Unwires all event handlers from the specified instance. + /// + /// The instance. + public void UnwireFrom(object instance) + { + if (instance == null || + EventProperties.Length + + DelegateProperties.Length == 0) + { + return; + } + + foreach (var eventInfo in EventProperties) + { + eventInfo.RemoveEventHandler(instance, this[eventInfo.Name]); + } + + foreach (var propInfo in DelegateProperties) + { + var value = propInfo.GetValue(instance, []) as Delegate; + value = Delegate.Remove(value, this[propInfo.Name]); + propInfo.SetValue(instance, value, []); + } + } + + /// + /// Adds the handler for the given event. + /// + /// The name of the event or delegate property. + /// The handler. + public void AddHandler(string name, Delegate handler) + { + DelegateHolders[name].AddHandler(handler); + } + + /// + /// Removes the handler for the given event. + /// + /// The name of the event or delegate property. + /// The handler. + public void RemoveHandler(string name, Delegate handler) + { + DelegateHolders[name].RemoveHandler(handler); + } + + /// + /// Gets the count of event handlers for the given event or delegate property. + /// + /// The event handler. + public static int GetHandlerCount(Delegate handler) + { + if (handler == null) + { + return 0; + } + + var count = 0; + foreach (var d in handler.GetInvocationList()) + { + // check if it's a delegate holder + if (d.Target is IDelegateHolder) + { + var holder = (IDelegateHolder)d.Target; + count += holder.HandlerCount; + continue; + } + + // it's an ordinary subscriber + count++; + } + + return count; + } +} diff --git a/CoreRemoting/RemotingSession.cs b/CoreRemoting/RemotingSession.cs index 61d2ba3..742bcd7 100644 --- a/CoreRemoting/RemotingSession.cs +++ b/CoreRemoting/RemotingSession.cs @@ -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) @@ -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; @@ -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 eventAccessor = subscription ? + eventStub.AddHandler : + eventStub.RemoveHandler; + + eventAccessor(eventName, eventHandler); + } + private MethodInfo GetMethodInfo(MethodCallMessage callMessage, Type serviceInterfaceType, Type[] parameterTypes) { MethodInfo method; diff --git a/CoreRemoting/ServerRpcContext.cs b/CoreRemoting/ServerRpcContext.cs index 28aad66..914cc80 100644 --- a/CoreRemoting/ServerRpcContext.cs +++ b/CoreRemoting/ServerRpcContext.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using CoreRemoting.DependencyInjection; +using CoreRemoting.RemoteDelegates; using CoreRemoting.RpcMessaging; namespace CoreRemoting @@ -49,6 +51,11 @@ public class ServerRpcContext /// public MethodCallResultMessage MethodCallResultMessage { get; set; } + /// + /// Gets or sets service event stub. + /// + public EventStub EventStub { get; set; } + /// /// Gets or sets the instance of the service, on which the method is called. /// diff --git a/CoreRemoting/Toolbox/Extensions.cs b/CoreRemoting/Toolbox/Extensions.cs new file mode 100644 index 0000000..46f623f --- /dev/null +++ b/CoreRemoting/Toolbox/Extensions.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace CoreRemoting.Toolbox +{ + /// + /// Extension methods. + /// + public static class Extensions + { + /// + /// Checks whether the given method represents event subscription or unsubscription. + /// + /// Method information. + /// If return value is true, this parameter returns the name of the event. + /// If true, method represents subscription, otherwise, it's unsubscription. + 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 DefaultValues = new(); + + /// + /// Gets the default value for the given type. + /// + /// The type. + /// default() for the type. + public static object GetDefaultValue(this Type type) + { + if (type == typeof(void) || !type.IsValueType) + { + return null; + } + + return DefaultValues.GetOrAdd(type, Activator.CreateInstance); + } + } +}