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

Add JsonRpc.AddLocalRpcTarget<T> method #519

Merged
merged 1 commit into from
Aug 11, 2020
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
83 changes: 83 additions & 0 deletions src/StreamJsonRpc.Tests/JsonRpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,87 @@ private interface IServer
int InstanceMethodWithSingleObjectParameterAndCancellationToken(XAndYFields fields, CancellationToken token);

int InstanceMethodWithSingleObjectParameterButNoAttribute(XAndYFields fields);

int Add_ExplicitInterfaceImplementation(int a, int b);
}

[Fact]
public async Task AddLocalRpcTarget_OfT_InterfaceOnly()
{
var streams = Nerdbank.FullDuplexStream.CreateStreams();
this.serverStream = streams.Item1;
this.clientStream = streams.Item2;

this.InitializeFormattersAndHandlers();

this.serverRpc = new JsonRpc(this.serverMessageHandler);
this.serverRpc.AddLocalRpcTarget<IServer>(this.server, null);
this.serverRpc.StartListening();

this.clientRpc = new JsonRpc(this.clientMessageHandler);
this.clientRpc.StartListening();

// Verify that members on the interface are callable.
await this.clientRpc.InvokeAsync("AnotherName", new object[] { "my -name" });

// Verify that explicitly interface implementations of members on the interface are callable.
Assert.Equal(3, await this.clientRpc.InvokeAsync<int>(nameof(IServer.Add_ExplicitInterfaceImplementation), 1, 2));

// Verify that members NOT on the interface are not callable, whether public or internal.
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(() => this.clientRpc.InvokeAsync(nameof(Server.AsyncMethod), new object[] { "my-name" }));
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(() => this.clientRpc.InvokeAsync(nameof(Server.InternalMethod)));
}

[Fact]
public async Task AddLocalRpcTarget_OfT_ActualClass()
{
var streams = Nerdbank.FullDuplexStream.CreateStreams();
this.serverStream = streams.Item1;
this.clientStream = streams.Item2;

this.InitializeFormattersAndHandlers();

this.serverRpc = new JsonRpc(this.serverMessageHandler);
this.serverRpc.AddLocalRpcTarget<Server>(this.server, null);
this.serverRpc.StartListening();

this.clientRpc = new JsonRpc(this.clientMessageHandler);
this.clientRpc.StartListening();

// Verify that public members on the class (and NOT the interface) are callable.
await this.clientRpc.InvokeAsync(nameof(Server.AsyncMethod), new object[] { "my-name" });

// Verify that explicitly interface implementations of members on the interface are NOT callable.
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(() => this.clientRpc.InvokeAsync<int>(nameof(IServer.Add_ExplicitInterfaceImplementation), 1, 2));

// Verify that internal members on the class are NOT callable.
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(() => this.clientRpc.InvokeAsync(nameof(Server.InternalMethod)));
}

[Fact]
public async Task AddLocalRpcTarget_OfT_ActualClass_NonPublicAccessible()
{
var streams = Nerdbank.FullDuplexStream.CreateStreams();
this.serverStream = streams.Item1;
this.clientStream = streams.Item2;

this.InitializeFormattersAndHandlers();

this.serverRpc = new JsonRpc(this.serverMessageHandler);
this.serverRpc.AddLocalRpcTarget<Server>(this.server, new JsonRpcTargetOptions { AllowNonPublicInvocation = true });
this.serverRpc.StartListening();

this.clientRpc = new JsonRpc(this.clientMessageHandler);
this.clientRpc.StartListening();

// Verify that public members on the class (and NOT the interface) are callable.
await this.clientRpc.InvokeAsync(nameof(Server.AsyncMethod), new object[] { "my-name" });

// Verify that explicitly interface implementations of members on the interface are callable.
await Assert.ThrowsAsync<RemoteMethodNotFoundException>(() => this.clientRpc.InvokeAsync<int>(nameof(IServer.Add_ExplicitInterfaceImplementation), 1, 2));

// Verify that internal members on the class are callable.
await this.clientRpc.InvokeAsync(nameof(Server.InternalMethod));
}

[Fact]
Expand Down Expand Up @@ -2529,6 +2610,8 @@ public void SendException(Exception? ex)
this.ReceivedException = ex;
}

int IServer.Add_ExplicitInterfaceImplementation(int a, int b) => a + b;

internal void InternalMethod()
{
this.ServerMethodReached.Set();
Expand Down
90 changes: 89 additions & 1 deletion src/StreamJsonRpc.Tests/TargetObjectEventsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Threading;
using Nerdbank;
Expand Down Expand Up @@ -42,6 +43,13 @@ public TargetObjectEventsTests(ITestOutputHelper logger)
this.clientRpc.TraceSource.Listeners.Add(new XunitTraceListener(this.Logger));
}

public interface IServer
{
event EventHandler InterfaceEvent;

event EventHandler ExplicitInterfaceImplementation_Event;
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down Expand Up @@ -194,6 +202,72 @@ public async Task NameTransformIsUsedWhenRaisingEvent()
Assert.True(eventNameTransformSeen);
}

[Fact]
public async Task AddLocalRpcTarget_OfT_Interface()
{
var streams = FullDuplexStream.CreateStreams();
this.serverStream = streams.Item1;
this.clientStream = streams.Item2;

this.serverRpc = new JsonRpc(this.serverStream);
this.serverRpc.AddLocalRpcTarget<IServer>(this.server, null);
this.serverRpc.StartListening();

this.clientRpc = new JsonRpc(this.clientStream);
var eventRaised = new TaskCompletionSource<EventArgs>();
this.clientRpc.AddLocalRpcMethod(nameof(IServer.InterfaceEvent), new Action<EventArgs>(eventRaised.SetResult));
var explicitEventRaised = new TaskCompletionSource<EventArgs>();
this.clientRpc.AddLocalRpcMethod(nameof(IServer.ExplicitInterfaceImplementation_Event), new Action<EventArgs>(explicitEventRaised.SetResult));
var classOnlyEventRaised = new TaskCompletionSource<EventArgs>();
this.clientRpc.AddLocalRpcMethod(nameof(Server.ServerEvent), new Action<EventArgs>(classOnlyEventRaised.SetResult));
this.clientRpc.StartListening();

// Verify that ordinary interface events can be raised.
this.server.TriggerInterfaceEvent(new EventArgs());
await eventRaised.Task.WithCancellation(this.TimeoutToken);

// Verify that explicit interface implementation events can also be raised.
this.server.TriggerExplicitInterfaceImplementationEvent(new EventArgs());
await explicitEventRaised.Task.WithCancellation(this.TimeoutToken);

// Verify that events that are NOT on the interface cannot be raised.
this.server.TriggerEvent(new EventArgs());
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => classOnlyEventRaised.Task.WithCancellation(ExpectedTimeoutToken));
}

[Fact]
public async Task AddLocalRpcTarget_OfT_ActualClass()
{
var streams = FullDuplexStream.CreateStreams();
this.serverStream = streams.Item1;
this.clientStream = streams.Item2;

this.serverRpc = new JsonRpc(this.serverStream);
this.serverRpc.AddLocalRpcTarget<Server>(this.server, null);
this.serverRpc.StartListening();

this.clientRpc = new JsonRpc(this.clientStream);
var eventRaised = new TaskCompletionSource<EventArgs>();
this.clientRpc.AddLocalRpcMethod(nameof(IServer.InterfaceEvent), new Action<EventArgs>(eventRaised.SetResult));
var explicitEventRaised = new TaskCompletionSource<EventArgs>();
this.clientRpc.AddLocalRpcMethod(nameof(IServer.ExplicitInterfaceImplementation_Event), new Action<EventArgs>(explicitEventRaised.SetResult));
var classOnlyEventRaised = new TaskCompletionSource<EventArgs>();
this.clientRpc.AddLocalRpcMethod(nameof(Server.ServerEvent), new Action<EventArgs>(classOnlyEventRaised.SetResult));
this.clientRpc.StartListening();

// Verify that ordinary interface events can be raised.
this.server.TriggerInterfaceEvent(new EventArgs());
await eventRaised.Task.WithCancellation(this.TimeoutToken);

// Verify that explicit interface implementation events can also be raised.
this.server.TriggerExplicitInterfaceImplementationEvent(new EventArgs());
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => explicitEventRaised.Task.WithCancellation(ExpectedTimeoutToken));

// Verify that events that are NOT on the interface can be raised.
this.server.TriggerEvent(new EventArgs());
await classOnlyEventRaised.Task.WithCancellation(this.TimeoutToken);
}

protected override void Dispose(bool disposing)
{
if (disposing)
Expand Down Expand Up @@ -224,8 +298,10 @@ private class Client
public void ServerEventWithCustomGenericDelegateAndArgs(MessageEventArgs<string> args) => this.ServerEventWithCustomGenericDelegateAndArgsRaised?.Invoke(args);
}

private class Server
private class Server : IServer
{
private EventHandler? explicitInterfaceImplementationEvent;

public delegate void MessageReceivedEventHandler<T>(object sender, MessageEventArgs<T> args)
where T : class;

Expand All @@ -237,8 +313,16 @@ public delegate void MessageReceivedEventHandler<T>(object sender, MessageEventA

public event MessageReceivedEventHandler<string>? ServerEventWithCustomGenericDelegateAndArgs;

public event EventHandler? InterfaceEvent;

private static event EventHandler? PrivateStaticServerEvent;

event EventHandler IServer.ExplicitInterfaceImplementation_Event
{
add => this.explicitInterfaceImplementationEvent += value;
remove => this.explicitInterfaceImplementationEvent -= value;
}

private event EventHandler? PrivateServerEvent;

internal EventHandler? ServerEventAccessor => this.ServerEvent;
Expand All @@ -259,6 +343,10 @@ public void TriggerGenericEvent(CustomEventArgs args)

public void TriggerServerEventWithCustomGenericDelegateAndArgs(MessageEventArgs<string> args) => this.OnServerEventWithCustomGenericDelegateAndArgs(args);

public void TriggerInterfaceEvent(EventArgs args) => this.InterfaceEvent?.Invoke(this, args);

public void TriggerExplicitInterfaceImplementationEvent(EventArgs args) => this.explicitInterfaceImplementationEvent?.Invoke(this, args);

protected static void OnPrivateStaticServerEvent(EventArgs args) => PrivateStaticServerEvent?.Invoke(null, args);

protected virtual void OnServerEvent(EventArgs args) => this.ServerEvent?.Invoke(this, args);
Expand Down
45 changes: 29 additions & 16 deletions src/StreamJsonRpc/JsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -684,26 +684,37 @@ public object Attach(Type interfaceType, JsonRpcProxyOptions? options)
return proxy;
}

/// <summary>
/// Adds the specified target as possible object to invoke when incoming messages are received. The target object
/// should not inherit from each other and are invoked in the order which they are added.
/// </summary>
/// <param name="target">Target to invoke when incoming messages are received.</param>
/// <inheritdoc cref="AddLocalRpcTarget(object, JsonRpcTargetOptions?)"/>
public void AddLocalRpcTarget(object target) => this.AddLocalRpcTarget(target, null);

/// <inheritdoc cref="AddLocalRpcTarget(Type, object, JsonRpcTargetOptions?)"/>
public void AddLocalRpcTarget(object target, JsonRpcTargetOptions? options) => this.AddLocalRpcTarget(Requires.NotNull(target, nameof(target)).GetType(), target, options);

/// <inheritdoc cref="AddLocalRpcTarget(Type, object, JsonRpcTargetOptions?)"/>
/// <typeparam name="T"><inheritdoc cref="AddLocalRpcTarget(Type, object, JsonRpcTargetOptions?)" path="/param[@name='exposingMembersOn']"/></typeparam>
public void AddLocalRpcTarget<T>(T target, JsonRpcTargetOptions? options)
where T : notnull => this.AddLocalRpcTarget(typeof(T), target, options);

/// <summary>
/// Adds the specified target as possible object to invoke when incoming messages are received. The target object
/// should not inherit from each other and are invoked in the order which they are added.
/// Adds the specified target as possible object to invoke when incoming messages are received.
/// </summary>
/// <param name="exposingMembersOn">
/// The type whose members define the RPC accessible members of the <paramref name="target"/> object.
/// If this type is not an interface, only public members become invokable unless <see cref="JsonRpcTargetOptions.AllowNonPublicInvocation"/> is set to true on the <paramref name="options"/> argument.
/// </param>
/// <param name="target">Target to invoke when incoming messages are received.</param>
/// <param name="options">A set of customizations for how the target object is registered. If <c>null</c>, default options will be used.</param>
public void AddLocalRpcTarget(object target, JsonRpcTargetOptions? options)
/// <remarks>
/// When multiple target objects are added, the first target with a method that matches a request is invoked.
/// </remarks>
public void AddLocalRpcTarget(Type exposingMembersOn, object target, JsonRpcTargetOptions? options)
{
Requires.NotNull(exposingMembersOn, nameof(exposingMembersOn));
Requires.NotNull(target, nameof(target));
options = options ?? JsonRpcTargetOptions.Default;
this.ThrowIfConfigurationLocked();

Dictionary<string, List<MethodSignatureAndTarget>> mapping = GetRequestMethodToClrMethodMap(target, options);
Dictionary<string, List<MethodSignatureAndTarget>> mapping = GetRequestMethodToClrMethodMap(exposingMembersOn.GetTypeInfo(), target, options);
lock (this.syncObject)
{
foreach (KeyValuePair<string, List<MethodSignatureAndTarget>> item in mapping)
Expand Down Expand Up @@ -742,11 +753,11 @@ public void AddLocalRpcTarget(object target, JsonRpcTargetOptions? options)

if (options.NotifyClientOfEvents)
{
for (TypeInfo? t = target.GetType().GetTypeInfo(); t != null && t != typeof(object).GetTypeInfo(); t = t.BaseType?.GetTypeInfo())
for (TypeInfo? t = exposingMembersOn.GetTypeInfo(); t != null && t != typeof(object).GetTypeInfo(); t = t.BaseType?.GetTypeInfo())
{
foreach (EventInfo evt in t.DeclaredEvents)
{
if ((evt.AddMethod?.IsPublic ?? false) && !evt.AddMethod.IsStatic)
if (evt.AddMethod is object && (evt.AddMethod.IsPublic || exposingMembersOn.IsInterface) && !evt.AddMethod.IsStatic)
{
if (this.eventReceivers == null)
{
Expand Down Expand Up @@ -1583,27 +1594,29 @@ protected virtual void OnRequestTransmissionAborted(JsonRpcRequest request)
/// <summary>
/// Creates a dictionary which maps a request method name to its clr method name via <see cref="JsonRpcMethodAttribute" /> value.
/// </summary>
/// <param name="target">Object to reflect over and analyze its methods.</param>
/// <param name="exposedMembersOnType">Type to reflect over and analyze its methods.</param>
/// <param name="target">The instance of <paramref name="exposedMembersOnType"/> to be exposed to RPC.</param>
/// <param name="options">The options that apply for this target object.</param>
/// <returns>Dictionary which maps a request method name to its clr method name.</returns>
private static Dictionary<string, List<MethodSignatureAndTarget>> GetRequestMethodToClrMethodMap(object target, JsonRpcTargetOptions options)
private static Dictionary<string, List<MethodSignatureAndTarget>> GetRequestMethodToClrMethodMap(TypeInfo exposedMembersOnType, object target, JsonRpcTargetOptions options)
{
Requires.NotNull(target, nameof(target));
Requires.NotNull(exposedMembersOnType, nameof(exposedMembersOnType));
Requires.NotNull(options, nameof(options));

var clrMethodToRequestMethodMap = new Dictionary<string, string>(StringComparer.Ordinal);
var requestMethodToClrMethodNameMap = new Dictionary<string, string>(StringComparer.Ordinal);
var requestMethodToDelegateMap = new Dictionary<string, List<MethodSignatureAndTarget>>(StringComparer.Ordinal);
var candidateAliases = new Dictionary<string, string>(StringComparer.Ordinal);

var mapping = new MethodNameMap(target.GetType().GetTypeInfo());
var mapping = new MethodNameMap(exposedMembersOnType);

for (TypeInfo? t = target.GetType().GetTypeInfo(); t != null && t != typeof(object).GetTypeInfo(); t = t.BaseType?.GetTypeInfo())
for (TypeInfo? t = exposedMembersOnType; t != null && t != typeof(object).GetTypeInfo(); t = t.BaseType?.GetTypeInfo())
{
// As we enumerate methods, skip accessor methods
foreach (MethodInfo method in t.DeclaredMethods.Where(m => !m.IsSpecialName))
{
if (!options.AllowNonPublicInvocation && !method.IsPublic)
if (!options.AllowNonPublicInvocation && !method.IsPublic && !exposedMembersOnType.IsInterface)
AArnott marked this conversation as resolved.
Show resolved Hide resolved
{
continue;
}
Expand Down
2 changes: 2 additions & 0 deletions src/StreamJsonRpc/netcoreapp2.1/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
StreamJsonRpc.JsonRpc.AddLocalRpcTarget(System.Type! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void
StreamJsonRpc.JsonRpc.AddLocalRpcTarget<T>(T target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void
StreamJsonRpc.JsonRpc.InvokeWithCancellationAsync(string! targetName, System.Collections.Generic.IReadOnlyList<object?>? arguments, System.Collections.Generic.IReadOnlyList<System.Type!>! argumentDeclaredTypes, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
StreamJsonRpc.JsonRpc.InvokeWithCancellationAsync<TResult>(string! targetName, System.Collections.Generic.IReadOnlyList<object?>? arguments, System.Collections.Generic.IReadOnlyList<System.Type!>? argumentDeclaredTypes, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<TResult>!
StreamJsonRpc.JsonRpc.TraceEvents.ExceptionTypeNotFound = 19 -> StreamJsonRpc.JsonRpc.TraceEvents
Expand Down
2 changes: 2 additions & 0 deletions src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
StreamJsonRpc.JsonRpc.AddLocalRpcTarget(System.Type! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void
StreamJsonRpc.JsonRpc.AddLocalRpcTarget<T>(T target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void
StreamJsonRpc.JsonRpc.InvokeWithCancellationAsync(string! targetName, System.Collections.Generic.IReadOnlyList<object?>? arguments, System.Collections.Generic.IReadOnlyList<System.Type!>! argumentDeclaredTypes, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
StreamJsonRpc.JsonRpc.InvokeWithCancellationAsync<TResult>(string! targetName, System.Collections.Generic.IReadOnlyList<object?>? arguments, System.Collections.Generic.IReadOnlyList<System.Type!>? argumentDeclaredTypes, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<TResult>!
StreamJsonRpc.JsonRpc.TraceEvents.ExceptionTypeNotFound = 19 -> StreamJsonRpc.JsonRpc.TraceEvents
Expand Down