diff --git a/docfx/docs/nativeAOT.md b/docfx/docs/nativeAOT.md index e52dcbe77..ddad62d0d 100644 --- a/docfx/docs/nativeAOT.md +++ b/docfx/docs/nativeAOT.md @@ -15,7 +15,7 @@ A consuming application can target NativeAOT while referencing StreamJsonRpc by 1. Use or instead of the default . provides the best and safest experience and greatest set of functionality when you can use MessagePack encoding, but must be used when UTF-8 JSON encoding is required. 1. Set on the property to the `Default` property on your class that derives from . -1. Use instead of . +1. Use to add RPC target objects rather than other overloads. 1. When constructing proxies, use the methods with `typeof` arguments or specific generic type arguments. 1. When using named parameters (e.g. or ), call the overloads that accept . 1. Avoid [RPC marshalable objects](../exotic_types/rpc_marshalable_objects.md). diff --git a/samples/NativeAOT/SystemTextJson.cs b/samples/NativeAOT/SystemTextJson.cs index 698b1c853..8715a0601 100644 --- a/samples/NativeAOT/SystemTextJson.cs +++ b/samples/NativeAOT/SystemTextJson.cs @@ -10,9 +10,13 @@ partial class SystemTextJson static async Task Main(string[] args) { (Stream clientPipe, Stream serverPipe) = FullDuplexStream.CreatePair(); - JsonRpc serverRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter())); - JsonRpc clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter())); - serverRpc.AddLocalRpcMethod(nameof(Server.AddAsync), new Server().AddAsync); + JsonRpc serverRpc = new(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter())); + JsonRpc clientRpc = new(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter())); + + RpcTargetMetadata.RegisterEventArgs(); + var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer))); + + serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null); serverRpc.StartListening(); IServer proxy = clientRpc.Attach(); clientRpc.StartListening(); @@ -37,12 +41,23 @@ partial class SourceGenerationContext : JsonSerializerContext; [JsonRpcContract] internal partial interface IServer { + event EventHandler Added; + Task AddAsync(int a, int b); } class Server : IServer { - public Task AddAsync(int a, int b) => Task.FromResult(a + b); + public event EventHandler? Added; + + public Task AddAsync(int a, int b) + { + int sum = a + b; + this.OnAdded(sum); + return Task.FromResult(sum); + } + + protected virtual void OnAdded(int sum) => this.Added?.Invoke(this, sum); } #endregion } diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index 90dd473fb..9e761616a 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -11,6 +11,7 @@ using System.Runtime.Serialization; using Microsoft.VisualStudio.Threading; using Newtonsoft.Json; +using StreamJsonRpc; using StreamJsonRpc.Protocol; using StreamJsonRpc.Reflection; @@ -921,7 +922,7 @@ public object Attach(ReadOnlySpan interfaceTypes, JsonRpcProxyOptions? opt public void AddLocalRpcTarget<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T target, JsonRpcTargetOptions? options) where T : notnull => this.AddLocalRpcTarget(typeof(T), target, options); - /// + /// /// Thrown if called after is called and is . [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public void AddLocalRpcTarget( @@ -932,9 +933,21 @@ public void AddLocalRpcTarget( Requires.NotNull(exposingMembersOn, nameof(exposingMembersOn)); Requires.NotNull(target, nameof(target)); this.ThrowIfConfigurationLocked(); + this.AddLocalRpcTargetInternal(exposingMembersOn, target, options, requestRevertOption: false); } + /// + public void AddLocalRpcTarget(RpcTargetMetadata exposingMembersOn, object target, JsonRpcTargetOptions? options) + { + Requires.NotNull(exposingMembersOn); + Requires.NotNull(target); + this.ThrowIfConfigurationLocked(); + + options ??= JsonRpcTargetOptions.Default; + this.rpcTargetInfo.AddLocalRpcTarget(exposingMembersOn, target, options, requestRevertOption: false); + } + /// /// Adds a remote rpc connection so calls can be forwarded to the remote target if local targets do not handle it. /// @@ -1440,7 +1453,20 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho this.rpcTargetInfo.AddLocalRpcMethod(handler, target, methodRpcSettings, synchronizationContext); } - /// + /// + /// Adds the specified target as possible object to invoke when incoming messages are received. + /// + /// + /// The type whose members define the RPC accessible members of the object. + /// If this type is not an interface, only public members become invokable unless is set to true on the argument. + /// + /// Target to invoke when incoming messages are received. + /// A set of customizations for how the target object is registered. If , default options will be used. + /// to receive an that can remove the target object; otherwise. + /// An object that may be disposed of to revert the addition of the target object. Will be null if and only if is . + /// + /// When multiple target objects are added, the first target with a method that matches a request is invoked. + /// [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] internal RpcTargetInfo.RevertAddLocalRpcTarget? AddLocalRpcTargetInternal( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, @@ -1448,21 +1474,30 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho JsonRpcTargetOptions? options, bool requestRevertOption) { - return this.rpcTargetInfo.AddLocalRpcTarget(exposingMembersOn, target, options, requestRevertOption); + RpcTargetMetadata.EnableDynamicEventHandlerCreation(); + + options ??= JsonRpcTargetOptions.Default; + RpcTargetMetadata mapping = + exposingMembersOn.IsInterface ? RpcTargetMetadata.FromInterface(exposingMembersOn) : + options.AllowNonPublicInvocation ? RpcTargetMetadata.FromClassNonPublic(exposingMembersOn) : + RpcTargetMetadata.FromClass(exposingMembersOn); + + return this.rpcTargetInfo.AddLocalRpcTarget(mapping, target, options, requestRevertOption); } /// /// Adds a new RPC interface to an existing target registering additional RPC methods. /// - /// The interface type whose members define the RPC accessible members of the object. + /// The interface type whose members define the RPC accessible members of the object. /// Target to invoke when incoming messages are received. /// A set of customizations for how the target object is registered. If , default options will be used. /// /// An optional object that may be disposed of to revert the addition of the target object. /// - internal void AddRpcInterfaceToTargetInternal([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, object target, JsonRpcTargetOptions? options, RpcTargetInfo.RevertAddLocalRpcTarget? revertAddLocalRpcTarget) + internal void AddRpcInterfaceToTargetInternal(RpcTargetMetadata targetMetadata, object target, JsonRpcTargetOptions? options, RpcTargetInfo.RevertAddLocalRpcTarget? revertAddLocalRpcTarget) { - this.rpcTargetInfo.AddRpcInterfaceToTarget(exposingMembersOn, target, options, revertAddLocalRpcTarget); + options ??= JsonRpcTargetOptions.Default; + this.rpcTargetInfo.AddRpcInterfaceToTarget(targetMetadata, target, options, revertAddLocalRpcTarget); } /// diff --git a/src/StreamJsonRpc/PolyfillMethods.cs b/src/StreamJsonRpc/PolyfillMethods.cs new file mode 100644 index 000000000..18ccf5ba3 --- /dev/null +++ b/src/StreamJsonRpc/PolyfillMethods.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace StreamJsonRpc; + +internal static class PolyfillMethods +{ +#if NETSTANDARD2_0 + internal static void Deconstruct(this KeyValuePair pair, out TKey key, out TValue value) + => (key, value) = (pair.Key, pair.Value); +#endif +} diff --git a/src/StreamJsonRpc/ProxyGeneration.cs b/src/StreamJsonRpc/ProxyGeneration.cs index b4c9ec077..7b4cae738 100644 --- a/src/StreamJsonRpc/ProxyGeneration.cs +++ b/src/StreamJsonRpc/ProxyGeneration.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Reflection.Emit; using Microsoft.VisualStudio.Threading; -using StreamJsonRpc.Reflection; using CodeGenHelpers = StreamJsonRpc.Reflection.CodeGenHelpers; // Uncomment the SaveAssembly symbol and run one test to save the generated DLL for inspection in ILSpy as part of debugging. @@ -70,6 +69,11 @@ internal static class ProxyGeneration internal static TypeInfo Get(Type contractInterface, ReadOnlySpan additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces) { Requires.NotNull(contractInterface, nameof(contractInterface)); + + // Dynamic proxy generation requires the ability to generate dynamic event handlers. + // Not a problem, since by calling into this method the user has already committed to running on a runtime that supports dynamic code. + RpcTargetMetadata.EnableDynamicEventHandlerCreation(); + VerifySupported(contractInterface.IsInterface, Resources.ClientProxyTypeArgumentMustBeAnInterface, contractInterface); foreach (TypeInfo additionalContract in additionalContractInterfaces) { @@ -293,171 +297,175 @@ internal static TypeInfo Get(Type contractInterface, ReadOnlySpan addition HashSet implementedMethods = new() { DisposeMethod }; foreach ((Type rpcInterface, int? rpcInterfaceCode) in rpcInterfaces) { - RpcTargetInfo.MethodNameMap methodNameMap = RpcTargetInfo.GetMethodNameMap(rpcInterface.GetTypeInfo()); - foreach (MethodInfo method in FindAllOnThisAndOtherInterfaces(rpcInterface.GetTypeInfo(), i => i.DeclaredMethods).Where(m => !m.IsSpecialName)) + RpcTargetMetadata methodNameMap = RpcTargetMetadata.FromInterface(rpcInterface.GetTypeInfo()); + foreach ((string name, IReadOnlyList overloads) in methodNameMap.Methods) { - if (!implementedMethods.Add(method)) + foreach (RpcTargetMetadata.TargetMethodMetadata methodMetadata in overloads) { - continue; - } + MethodInfo method = methodMetadata.Method; + if (!implementedMethods.Add(method)) + { + continue; + } - bool returnTypeIsTask = method.ReturnType == typeof(Task) || (method.ReturnType.GetTypeInfo().IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)); - bool returnTypeIsValueTask = method.ReturnType == typeof(ValueTask) || (method.ReturnType.GetTypeInfo().IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)); - bool returnTypeIsIAsyncEnumerable = method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>); - bool returnTypeIsVoid = method.ReturnType == typeof(void); - VerifySupported(returnTypeIsVoid || returnTypeIsTask || returnTypeIsValueTask || returnTypeIsIAsyncEnumerable, Resources.UnsupportedMethodReturnTypeOnClientProxyInterface, method, method.ReturnType.FullName!); - VerifySupported(!method.IsGenericMethod, Resources.UnsupportedGenericMethodsOnClientProxyInterface, method); - - bool hasReturnValue = method.ReturnType.GetTypeInfo().IsGenericType; - Type? invokeResultTypeArgument = hasReturnValue - ? (returnTypeIsIAsyncEnumerable ? method.ReturnType : method.ReturnType.GetTypeInfo().GenericTypeArguments[0]) - : null; - - string methodName = method.Name; - string rpcMethodName = methodNameMap.GetRpcMethodName(method); - if (rpcInterfaceCode.HasValue) - { - methodName = $"{rpcInterfaceCode.GetValueOrDefault()}.{method.Name}"; - rpcMethodName = $"{rpcInterfaceCode.GetValueOrDefault()}.{rpcMethodName}"; - } + bool returnTypeIsTask = method.ReturnType == typeof(Task) || (method.ReturnType.GetTypeInfo().IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)); + bool returnTypeIsValueTask = method.ReturnType == typeof(ValueTask) || (method.ReturnType.GetTypeInfo().IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)); + bool returnTypeIsIAsyncEnumerable = method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>); + bool returnTypeIsVoid = method.ReturnType == typeof(void); + VerifySupported(returnTypeIsVoid || returnTypeIsTask || returnTypeIsValueTask || returnTypeIsIAsyncEnumerable, Resources.UnsupportedMethodReturnTypeOnClientProxyInterface, method, method.ReturnType.FullName!); + VerifySupported(!method.IsGenericMethod, Resources.UnsupportedGenericMethodsOnClientProxyInterface, method); + + bool hasReturnValue = method.ReturnType.GetTypeInfo().IsGenericType; + Type? invokeResultTypeArgument = hasReturnValue + ? (returnTypeIsIAsyncEnumerable ? method.ReturnType : method.ReturnType.GetTypeInfo().GenericTypeArguments[0]) + : null; + + string methodName = method.Name; + string rpcMethodName = name; + if (rpcInterfaceCode.HasValue) + { + methodName = $"{rpcInterfaceCode.GetValueOrDefault()}.{method.Name}"; + rpcMethodName = $"{rpcInterfaceCode.GetValueOrDefault()}.{rpcMethodName}"; + } - ParameterInfo[] methodParameters = method.GetParameters(); - MethodBuilder methodBuilder = proxyTypeBuilder.DefineMethod( - methodName, - MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, - method.ReturnType, - methodParameters.Select(p => p.ParameterType).ToArray()); - ILGenerator il = methodBuilder.GetILGenerator(); + ParameterInfo[] methodParameters = method.GetParameters(); + MethodBuilder methodBuilder = proxyTypeBuilder.DefineMethod( + methodName, + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, + method.ReturnType, + methodParameters.Select(p => p.ParameterType).ToArray()); + ILGenerator il = methodBuilder.GetILGenerator(); - EmitThrowIfDisposed(proxyTypeBuilder, il, disposedField); + EmitThrowIfDisposed(proxyTypeBuilder, il, disposedField); - EmitRaiseCallEvent(il, callingMethodField, methodName); + EmitRaiseCallEvent(il, callingMethodField, methodName); - // this.rpc - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, jsonRpcField); + // this.rpc + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, jsonRpcField); - // First argument to InvokeAsync and NotifyAsync is the method name. - // Run it through the method name transform. - // this.options.MethodNameTransform.Invoke("clrOrAttributedMethodName") - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, optionsField); - il.EmitCall(OpCodes.Callvirt, MethodNameTransformPropertyGetter, null); - il.Emit(OpCodes.Ldstr, rpcMethodName); - il.EmitCall(OpCodes.Callvirt, MethodNameTransformInvoke, null); + // First argument to InvokeAsync and NotifyAsync is the method name. + // Run it through the method name transform. + // this.options.MethodNameTransform.Invoke("clrOrAttributedMethodName") + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, optionsField); + il.EmitCall(OpCodes.Callvirt, MethodNameTransformPropertyGetter, null); + il.Emit(OpCodes.Ldstr, rpcMethodName); + il.EmitCall(OpCodes.Callvirt, MethodNameTransformInvoke, null); - Label positionalArgsLabel = il.DefineLabel(); + Label positionalArgsLabel = il.DefineLabel(); - ParameterInfo? cancellationTokenParameter = methodParameters.FirstOrDefault(p => p.ParameterType == typeof(CancellationToken)); - int argumentCountExcludingCancellationToken = methodParameters.Length - (cancellationTokenParameter is not null ? 1 : 0); - VerifySupported(cancellationTokenParameter is null || cancellationTokenParameter.Position == methodParameters.Length - 1, Resources.CancellationTokenMustBeLastParameter, method); + ParameterInfo? cancellationTokenParameter = methodParameters.FirstOrDefault(p => p.ParameterType == typeof(CancellationToken)); + int argumentCountExcludingCancellationToken = methodParameters.Length - (cancellationTokenParameter is not null ? 1 : 0); + VerifySupported(cancellationTokenParameter is null || cancellationTokenParameter.Position == methodParameters.Length - 1, Resources.CancellationTokenMustBeLastParameter, method); - // if (this.options.ServerRequiresNamedArguments) { - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, optionsField); - il.EmitCall(OpCodes.Callvirt, ServerRequiresNamedArgumentsPropertyGetter, null); - il.Emit(OpCodes.Brfalse, positionalArgsLabel); + // if (this.options.ServerRequiresNamedArguments) { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, optionsField); + il.EmitCall(OpCodes.Callvirt, ServerRequiresNamedArgumentsPropertyGetter, null); + il.Emit(OpCodes.Brfalse, positionalArgsLabel); - // The second argument is a single parameter object. - { - if (argumentCountExcludingCancellationToken > 0) + // The second argument is a single parameter object. { - ConstructorInfo paramObjectCtor = CreateParameterObjectType(proxyModuleBuilder, methodParameters.Take(argumentCountExcludingCancellationToken).ToArray()); - for (int i = 0; i < argumentCountExcludingCancellationToken; i++) + if (argumentCountExcludingCancellationToken > 0) { - il.Emit(OpCodes.Ldarg, i + 1); - } - - il.Emit(OpCodes.Newobj, paramObjectCtor); - } - else - { - il.Emit(OpCodes.Ldnull); - } + ConstructorInfo paramObjectCtor = CreateParameterObjectType(proxyModuleBuilder, methodParameters.Take(argumentCountExcludingCancellationToken).ToArray()); + for (int i = 0; i < argumentCountExcludingCancellationToken; i++) + { + il.Emit(OpCodes.Ldarg, i + 1); + } - // Note that we do NOT need to load in a dictionary of named parameter types - // because of our specialized parameter object that strongly types all arguments for us. + il.Emit(OpCodes.Newobj, paramObjectCtor); + } + else + { + il.Emit(OpCodes.Ldnull); + } - // Construct the InvokeAsync method with the T argument supplied if we have a return type. - MethodInfo invokingMethod = - invokeResultTypeArgument is not null ? invokeWithParameterObjectAsyncOfTaskOfTMethodInfo.MakeGenericMethod(invokeResultTypeArgument) : - returnTypeIsVoid ? notifyWithParameterObjectAsyncOfTaskMethodInfo : - invokeWithParameterObjectAsyncOfTaskMethodInfo; + // Note that we do NOT need to load in a dictionary of named parameter types + // because of our specialized parameter object that strongly types all arguments for us. - CompleteCall(invokingMethod); - } + // Construct the InvokeAsync method with the T argument supplied if we have a return type. + MethodInfo invokingMethod = + invokeResultTypeArgument is not null ? invokeWithParameterObjectAsyncOfTaskOfTMethodInfo.MakeGenericMethod(invokeResultTypeArgument) : + returnTypeIsVoid ? notifyWithParameterObjectAsyncOfTaskMethodInfo : + invokeWithParameterObjectAsyncOfTaskMethodInfo; - // The second argument is an array of arguments for the RPC method. - il.MarkLabel(positionalArgsLabel); - { - if (argumentCountExcludingCancellationToken == 0) - { - // No args, so avoid creating an array. - il.Emit(OpCodes.Ldnull); + CompleteCall(invokingMethod); } - else - { - il.Emit(OpCodes.Ldc_I4, argumentCountExcludingCancellationToken); - il.Emit(OpCodes.Newarr, typeof(object)); - for (int i = 0; i < argumentCountExcludingCancellationToken; i++) + // The second argument is an array of arguments for the RPC method. + il.MarkLabel(positionalArgsLabel); + { + if (argumentCountExcludingCancellationToken == 0) + { + // No args, so avoid creating an array. + il.Emit(OpCodes.Ldnull); + } + else { - il.Emit(OpCodes.Dup); // duplicate the array on the stack - il.Emit(OpCodes.Ldc_I4, i); // push the index of the array to be initialized. - il.Emit(OpCodes.Ldarg, i + 1); // push the associated argument - if (methodParameters[i].ParameterType.GetTypeInfo().IsValueType) + il.Emit(OpCodes.Ldc_I4, argumentCountExcludingCancellationToken); + il.Emit(OpCodes.Newarr, typeof(object)); + + for (int i = 0; i < argumentCountExcludingCancellationToken; i++) { - il.Emit(OpCodes.Box, methodParameters[i].ParameterType); // box if the argument is a value type + il.Emit(OpCodes.Dup); // duplicate the array on the stack + il.Emit(OpCodes.Ldc_I4, i); // push the index of the array to be initialized. + il.Emit(OpCodes.Ldarg, i + 1); // push the associated argument + if (methodParameters[i].ParameterType.GetTypeInfo().IsValueType) + { + il.Emit(OpCodes.Box, methodParameters[i].ParameterType); // box if the argument is a value type + } + + il.Emit(OpCodes.Stelem_Ref); // set the array element. } - - il.Emit(OpCodes.Stelem_Ref); // set the array element. } - } - // The third argument is a Type[] describing each parameter type. - LoadParameterTypeArrayField(proxyTypeBuilder, methodParameters.Take(argumentCountExcludingCancellationToken).ToArray(), il); + // The third argument is a Type[] describing each parameter type. + LoadParameterTypeArrayField(proxyTypeBuilder, methodParameters.Take(argumentCountExcludingCancellationToken).ToArray(), il); - // Construct the InvokeAsync method with the T argument supplied if we have a return type. - MethodInfo invokingMethod = - invokeResultTypeArgument is object ? invokeWithCancellationAsyncOfTaskOfTMethodInfo.MakeGenericMethod(invokeResultTypeArgument) : - returnTypeIsVoid ? NotifyAsyncOfTaskMethodInfo : - invokeWithCancellationAsyncOfTaskMethodInfo; + // Construct the InvokeAsync method with the T argument supplied if we have a return type. + MethodInfo invokingMethod = + invokeResultTypeArgument is object ? invokeWithCancellationAsyncOfTaskOfTMethodInfo.MakeGenericMethod(invokeResultTypeArgument) : + returnTypeIsVoid ? NotifyAsyncOfTaskMethodInfo : + invokeWithCancellationAsyncOfTaskMethodInfo; - CompleteCall(invokingMethod); - } + CompleteCall(invokingMethod); + } - proxyTypeBuilder.DefineMethodOverride(methodBuilder, method); + proxyTypeBuilder.DefineMethodOverride(methodBuilder, method); - void CompleteCall(MethodInfo invokingMethod) - { - // Only pass in the CancellationToken argument if we're NOT calling the Notify method (which doesn't take one). - if (!returnTypeIsVoid) + void CompleteCall(MethodInfo invokingMethod) { - if (cancellationTokenParameter is not null) + // Only pass in the CancellationToken argument if we're NOT calling the Notify method (which doesn't take one). + if (!returnTypeIsVoid) { - il.Emit(OpCodes.Ldarg, cancellationTokenParameter.Position + 1); + if (cancellationTokenParameter is not null) + { + il.Emit(OpCodes.Ldarg, cancellationTokenParameter.Position + 1); + } + else + { + il.Emit(OpCodes.Call, CancellationTokenNonePropertyGetter); + } + } + + il.EmitCall(OpCodes.Callvirt, invokingMethod, null); + + if (returnTypeIsVoid) + { + // Disregard the Task returned by NotifyAsync. + il.Emit(OpCodes.Pop); } else { - il.Emit(OpCodes.Call, CancellationTokenNonePropertyGetter); + AdaptReturnType(method, returnTypeIsValueTask, returnTypeIsIAsyncEnumerable, il, invokingMethod, cancellationTokenParameter); } - } - il.EmitCall(OpCodes.Callvirt, invokingMethod, null); + EmitRaiseCallEvent(il, calledMethodField, method.Name); - if (returnTypeIsVoid) - { - // Disregard the Task returned by NotifyAsync. - il.Emit(OpCodes.Pop); - } - else - { - AdaptReturnType(method, returnTypeIsValueTask, returnTypeIsIAsyncEnumerable, il, invokingMethod, cancellationTokenParameter); + il.Emit(OpCodes.Ret); } - - EmitRaiseCallEvent(il, calledMethodField, method.Name); - - il.Emit(OpCodes.Ret); } } } diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs index 6315cd755..1374d33e8 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs @@ -258,7 +258,7 @@ internal MarshalToken GetToken( optionalInterfacesCodes.Add(attribute.OptionalInterfaceCode); this.jsonRpc.AddRpcInterfaceToTargetInternal( - attribute.OptionalInterface, + RpcTargetMetadata.FromInterface(attribute.OptionalInterface), context.Proxy, new JsonRpcTargetOptions(context.JsonRpcTargetOptions) { diff --git a/src/StreamJsonRpc/Reflection/MethodSignature.cs b/src/StreamJsonRpc/Reflection/MethodSignature.cs deleted file mode 100644 index d8364cf14..000000000 --- a/src/StreamJsonRpc/Reflection/MethodSignature.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace StreamJsonRpc; - -[DebuggerDisplay("{DebuggerDisplay}")] -internal sealed class MethodSignature : IEquatable -{ - private static readonly StringComparer TypeNameComparer = StringComparer.Ordinal; - - /// - /// Backing field for the lazily initialized property. - /// - private ParameterInfo[]? parameters; - - internal MethodSignature(MethodInfo methodInfo, JsonRpcMethodAttribute? attribute) - { - Requires.NotNull(methodInfo, nameof(methodInfo)); - this.MethodInfo = methodInfo; - this.Attribute = attribute; - } - - internal MethodInfo MethodInfo { get; } - - internal JsonRpcMethodAttribute? Attribute { get; } - - internal ParameterInfo[] Parameters => this.parameters ?? (this.parameters = this.MethodInfo.GetParameters() ?? Array.Empty()); - - internal bool IsPublic => this.MethodInfo.IsPublic; - - internal string Name => this.MethodInfo.Name; - - internal int RequiredParamCount => this.Parameters.Count(pi => !pi.IsOptional && !IsCancellationToken(pi)); - - internal int TotalParamCountExcludingCancellationToken => this.HasCancellationTokenParameter ? this.Parameters.Length - 1 : this.Parameters.Length; - - internal bool HasCancellationTokenParameter => this.Parameters.Length > 0 && this.Parameters[this.Parameters.Length - 1].ParameterType == typeof(CancellationToken); - - internal bool HasOutOrRefParameters => this.Parameters.Any(pi => pi.IsOut || pi.ParameterType.IsByRef); - - [ExcludeFromCodeCoverage] - private string DebuggerDisplay => $"{this.MethodInfo.DeclaringType}.{this.Name}({string.Join(", ", this.Parameters.Select(p => p.ParameterType.Name))})"; - - /// - public bool Equals(MethodSignature? other) - { - if (other is null) - { - return false; - } - - if (object.ReferenceEquals(other, this) || object.ReferenceEquals(this.Parameters, other.Parameters)) - { - return true; - } - - if (this.Parameters.Length != other.Parameters.Length) - { - return false; - } - - for (int index = 0; index < this.Parameters.Length; index++) - { - if (!MethodSignature.TypeNameComparer.Equals( - this.Parameters[index].ParameterType.AssemblyQualifiedName, - other.Parameters[index].ParameterType.AssemblyQualifiedName)) - { - return false; - } - } - - // We intentionally omit equating the MethodInfo itself because we want to consider - // overrides to be equal across types in the type hierarchy. - return true; - } - - /// - public override bool Equals(object? obj) => obj is MethodSignature other && this.Equals(other); - - /// - public override int GetHashCode() - { - uint result = 0; - int bitCount = sizeof(uint) * 8; - const int shift = 1; - - foreach (ParameterInfo parameter in this.Parameters) - { - // Shifting result 1 bit per each parameter so that the hash is different for - // methods with the same parameter types at different location, e.g. - // foo(int, string) and foo(string, int) - // This will work fine for up to 32 (64 on x64) parameters, - // which should be more than enough for the most applications. - result = result << shift | result >> (bitCount - shift); - result ^= (uint)MethodSignature.TypeNameComparer.GetHashCode(parameter.ParameterType.AssemblyQualifiedName!); - } - - return (int)result; - } - - /// - public override string ToString() - { - return this.DebuggerDisplay; - } - - internal bool MatchesParametersExcludingCancellationToken(ReadOnlySpan parameters) - { - if (this.TotalParamCountExcludingCancellationToken == parameters.Length) - { - for (int i = 0; i < parameters.Length; i++) - { - if (parameters[i].ParameterType != this.Parameters[i].ParameterType) - { - return false; - } - } - - return true; - } - - return false; - } - - private static bool IsCancellationToken(ParameterInfo parameter) => parameter?.ParameterType.Equals(typeof(CancellationToken)) ?? false; -} diff --git a/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs b/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs index f4e8cbd9d..c99f45583 100644 --- a/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs +++ b/src/StreamJsonRpc/Reflection/MethodSignatureAndTarget.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using System.Runtime.CompilerServices; namespace StreamJsonRpc; @@ -11,23 +10,17 @@ namespace StreamJsonRpc; [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] internal struct MethodSignatureAndTarget : IEquatable { - internal MethodSignatureAndTarget(MethodInfo method, object? target, JsonRpcMethodAttribute? attribute, SynchronizationContext? perMethodSynchronizationContext) - { - Requires.NotNull(method, nameof(method)); - - this.Signature = new MethodSignature(method, attribute); - this.Target = target; - this.SynchronizationContext = perMethodSynchronizationContext; - } - - internal MethodSignatureAndTarget(MethodSignature signature, object? target, SynchronizationContext? perMethodSynchronizationContext) + internal MethodSignatureAndTarget(RpcTargetMetadata.TargetMethodMetadata signature, object? target, JsonRpcMethodAttribute? attribute, SynchronizationContext? perMethodSynchronizationContext) { this.Signature = signature; this.Target = target; this.SynchronizationContext = perMethodSynchronizationContext; + this.Attribute = attribute ?? signature.Attribute; } - internal MethodSignature Signature { get; } + internal RpcTargetMetadata.TargetMethodMetadata Signature { get; } + + internal JsonRpcMethodAttribute? Attribute { get; } internal object? Target { get; } diff --git a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs index cd4e1320c..51829ba71 100644 --- a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs +++ b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs @@ -3,25 +3,23 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Reflection; using System.Runtime.ExceptionServices; -using Microsoft.VisualStudio.Threading; using StreamJsonRpc.Protocol; namespace StreamJsonRpc.Reflection; internal class RpcTargetInfo : System.IAsyncDisposable { - private const string ImpliedMethodNameAsyncSuffix = "Async"; - private static readonly Dictionary MethodNameMaps = new Dictionary(); - private static readonly Dictionary<(TypeInfo Type, bool AllowNonPublicInvocation, bool UseSingleObjectParameterDeserialization, bool ClientRequiresNamedArguments), Dictionary>> RequestMethodToClrMethodMap = new(); private readonly JsonRpc jsonRpc; /// /// A collection of target objects and their map of clr method to values. /// - private readonly Dictionary> targetRequestMethodToClrMethodMap = new Dictionary>(StringComparer.Ordinal); + /// + /// Access to this collection should be guarded by . + /// + private readonly Dictionary> targetRequestMethodToClrMethodMap = new(StringComparer.Ordinal); /// /// A list of event handlers we've registered on target objects that define events. May be if there are no handlers. @@ -94,32 +92,6 @@ public async ValueTask DisposeAsync() } } - internal static MethodNameMap GetMethodNameMap([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo type) - { - MethodNameMap? map; - lock (MethodNameMaps) - { - if (MethodNameMaps.TryGetValue(type, out map)) - { - return map; - } - } - - map = new MethodNameMap(type); - - lock (MethodNameMaps) - { - if (MethodNameMaps.TryGetValue(type, out MethodNameMap? lostRaceMap)) - { - return lostRaceMap; - } - - MethodNameMaps.Add(type, map); - } - - return map; - } - /// /// Gets the for a previously discovered RPC method, if there is one. /// @@ -138,7 +110,7 @@ internal static MethodNameMap GetMethodNameMap([DynamicallyAccessedMembers(Dynam { if (entry.Signature.MatchesParametersExcludingCancellationToken(parameters)) { - return entry.Signature.Attribute; + return entry.Attribute; } } } @@ -173,62 +145,38 @@ internal bool TryGetTargetMethod(JsonRpcRequest request, [NotNullWhen(true)] out /// /// Adds the specified target as possible object to invoke when incoming messages are received. /// - /// - /// The type whose members define the RPC accessible members of the object. - /// If this type is not an interface, only public members become invokable unless is set to true on the argument. - /// + /// The description of the RPC target. /// Target to invoke when incoming messages are received. /// A set of customizations for how the target object is registered. If , default options will be used. - /// to receive an that can remove the target object; otherwise. - /// An object that may be disposed of to revert the addition of the target object. Will be null if and only if is . + /// An optional object that may be disposed of to revert the addition of the target object. /// /// When multiple target objects are added, the first target with a method that matches a request is invoked. /// - [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] - internal RevertAddLocalRpcTarget? AddLocalRpcTarget( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, + internal void AddLocalRpcTarget( + RpcTargetMetadata targetType, object target, - JsonRpcTargetOptions? options, - bool requestRevertOption) + JsonRpcTargetOptions options, + RevertAddLocalRpcTarget? revertAddLocalRpcTarget) { - RevertAddLocalRpcTarget? revert = requestRevertOption ? new RevertAddLocalRpcTarget(this) : null; - options = options ?? JsonRpcTargetOptions.Default; - IReadOnlyDictionary> mapping = GetRequestMethodToClrMethodMap(exposingMembersOn.GetTypeInfo(), options.AllowNonPublicInvocation, options.UseSingleObjectParameterDeserialization, options.ClientRequiresNamedArguments); + Requires.Argument(targetType.TargetType.IsAssignableFrom(target.GetType()), nameof(target), "Target object must be assignable to the target type."); lock (this.SyncObject) { - this.AddRpcInterfaceToTarget(mapping, target, options, revert); + this.AddRpcInterfaceToTarget(targetType, target, options, revertAddLocalRpcTarget); if (options.NotifyClientOfEvents) { - HashSet? eventsDiscovered = null; - IReadOnlyList events = GetEventInfos(exposingMembersOn.GetTypeInfo()); - - foreach (EventInfo evt in events) + foreach (RpcTargetMetadata.EventMetadata evt in targetType.Events) { - if (this.eventReceivers is null) - { - this.eventReceivers = new List(); - } + this.eventReceivers ??= []; - if (eventsDiscovered is null) + if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Verbose)) { - eventsDiscovered = new HashSet(StringComparer.Ordinal); - } - - if (!eventsDiscovered.Add(evt.Name)) - { - // Do not add the same event again. It can appear multiple times in a type hierarchy. - continue; - } - - if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)JsonRpc.TraceEvents.LocalEventListenerAdded, "Listening for events from {0}.{1} to raise notification.", target.GetType().FullName, evt.Name); + this.TraceSource.TraceEvent(TraceEventType.Verbose, (int)JsonRpc.TraceEvents.LocalEventListenerAdded, "Listening for events from {0}.{1} to raise notification.", target.GetType().FullName, evt.Name); } var eventReceiver = new EventReceiver(this.jsonRpc, target, evt, options); - revert?.RecordEventReceiver(eventReceiver); + revertAddLocalRpcTarget?.RecordEventReceiver(eventReceiver); this.eventReceivers.Add(eventReceiver); } } @@ -240,29 +188,32 @@ internal bool TryGetTargetMethod(JsonRpcRequest request, [NotNullWhen(true)] out this.localTargetObjectsToDispose = new List(); } - revert?.RecordObjectToDispose(target); + revertAddLocalRpcTarget?.RecordObjectToDispose(target); this.localTargetObjectsToDispose.Add(target); } } - - return revert; } /// - /// Adds a new RPC interface to an existing target registering additional RPC methods. + /// Adds the specified target as possible object to invoke when incoming messages are received. /// - /// The interface type whose members define the RPC accessible members of the object. + /// The description of the RPC target. /// Target to invoke when incoming messages are received. /// A set of customizations for how the target object is registered. If , default options will be used. - /// An optional object that may be disposed of to revert the addition of the target object.. - internal void AddRpcInterfaceToTarget([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, object target, JsonRpcTargetOptions? options, RevertAddLocalRpcTarget? revertAddLocalRpcTarget) + /// to receive an that can remove the target object; otherwise. + /// An object that may be disposed of to revert the addition of the target object. Will be null if and only if is . + /// + /// When multiple target objects are added, the first target with a method that matches a request is invoked. + /// + internal RevertAddLocalRpcTarget? AddLocalRpcTarget( + RpcTargetMetadata exposingMembersOn, + object target, + JsonRpcTargetOptions options, + bool requestRevertOption) { - Requires.Argument(exposingMembersOn.IsInterface, nameof(exposingMembersOn), Resources.AddRpcInterfaceToTargetParameterNotInterface); - - options = options ?? JsonRpcTargetOptions.Default; - IReadOnlyDictionary> mapping = GetRequestMethodToClrMethodMap(exposingMembersOn.GetTypeInfo(), allowNonPublicInvocation: true, options.UseSingleObjectParameterDeserialization, options.ClientRequiresNamedArguments); - - this.AddRpcInterfaceToTarget(mapping, target, options, revertAddLocalRpcTarget); + RevertAddLocalRpcTarget? revert = requestRevertOption ? new RevertAddLocalRpcTarget(this) : null; + this.AddLocalRpcTarget(exposingMembersOn, target, options, revert); + return revert; } /// @@ -287,11 +238,11 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho string rpcMethodName = methodRpcSettings?.Name ?? handler.Name; lock (this.SyncObject) { - var methodTarget = new MethodSignatureAndTarget(handler, target, methodRpcSettings, synchronizationContext); + MethodSignatureAndTarget methodTarget = new(RpcTargetMetadata.TargetMethodMetadata.From(handler, methodRpcSettings), target, attribute: null, synchronizationContext); this.TraceLocalMethodAdded(rpcMethodName, methodTarget); if (this.targetRequestMethodToClrMethodMap.TryGetValue(rpcMethodName, out List? existingList)) { - if (existingList.Any(m => m.Signature.Equals(methodTarget.Signature))) + if (existingList.Any(m => m.Signature.EqualSignature(methodTarget.Signature))) { throw new InvalidOperationException(Resources.ConflictMethodSignatureAlreadyRegistered); } @@ -318,227 +269,22 @@ internal void UnregisterEventHandlersFromTargetObjects() } } - /// - /// Gets a dictionary which maps a request method name to its clr method name via value. - /// - /// Type to reflect over and analyze its methods. - /// - /// - /// - /// Dictionary which maps a request method name to its clr method name. - private static IReadOnlyDictionary> GetRequestMethodToClrMethodMap( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo exposedMembersOnType, - bool allowNonPublicInvocation, - bool useSingleObjectParameterDeserialization, - bool clientRequiresNamedArguments) - { - Requires.NotNull(exposedMembersOnType, nameof(exposedMembersOnType)); - - (TypeInfo Type, bool AllowNonPublicInvocation, bool UseSingleObjectParameterDeserialization, bool ClientRequiresNamedArguments) key = (exposedMembersOnType, allowNonPublicInvocation, useSingleObjectParameterDeserialization, clientRequiresNamedArguments); - Dictionary>? requestMethodToDelegateMap; - lock (RequestMethodToClrMethodMap) - { - if (RequestMethodToClrMethodMap.TryGetValue(key, out requestMethodToDelegateMap)) - { - return requestMethodToDelegateMap; - } - } - - requestMethodToDelegateMap = new Dictionary>(StringComparer.Ordinal); - var clrMethodToRequestMethodMap = new Dictionary(StringComparer.Ordinal); - var requestMethodToClrMethodNameMap = new Dictionary(StringComparer.Ordinal); - var candidateAliases = new Dictionary(StringComparer.Ordinal); - - MethodNameMap mapping = GetMethodNameMap(exposedMembersOnType); - - // We retrieve exposed types differently for interfaces vs. classes - if (exposedMembersOnType.IsInterface) - { - ActOn(exposedMembersOnType.GetTypeInfo()); - ActOnInterfaces(exposedMembersOnType); - } - else - { - for (TypeInfo? t = exposedMembersOnType.GetTypeInfo(); t is not null && t != typeof(object).GetTypeInfo(); t = t.BaseType?.GetTypeInfo()) - { - ActOn(t); - } - } - -#if !NET10_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2062:UnrecognizedReflectionPattern", Justification = "exposedMembersOnType is annotated as preserve All members, so any Types returned from GetInterfaces should be preserved as well. See https://github.com/dotnet/linker/issues/1731.")] -#endif - void ActOnInterfaces([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposedMembersOnType) - { - Type[] interfaces = exposedMembersOnType.GetInterfaces(); - for (int i = 0; i < interfaces.Length; i++) - { - ActOn(interfaces[i].GetTypeInfo()); - } - } - - void ActOn([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TypeInfo t) - { - // As we enumerate methods, skip accessor methods - foreach (MethodInfo method in t.DeclaredMethods.Where(m => !m.IsSpecialName)) - { - if (!key.AllowNonPublicInvocation && !method.IsPublic && !exposedMembersOnType.IsInterface) - { - continue; - } - - if (mapping.FindIgnoreAttribute(method) is object) - { - if (mapping.FindMethodAttribute(method) is object) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.JsonRpcMethodAndIgnoreAttributesFound, method.Name)); - } - - continue; - } - - var requestName = mapping.GetRpcMethodName(method); - - if (!requestMethodToDelegateMap.TryGetValue(requestName, out List? methodList)) - { - methodList = new List(); - requestMethodToDelegateMap.Add(requestName, methodList); - } - - // Verify that all overloads of this CLR method also claim the same request method name. - if (clrMethodToRequestMethodMap.TryGetValue(method.Name, out string? previousRequestNameUse)) - { - if (!string.Equals(previousRequestNameUse, requestName, StringComparison.Ordinal)) - { - Requires.Fail(Resources.ConflictingMethodNameAttribute, method.Name, nameof(JsonRpcMethodAttribute), nameof(JsonRpcMethodAttribute.Name)); - } - } - else - { - clrMethodToRequestMethodMap.Add(method.Name, requestName); - } - - // Verify that all CLR methods that want to use this request method name are overloads of each other. - if (requestMethodToClrMethodNameMap.TryGetValue(requestName, out string? previousClrNameUse)) - { - if (!string.Equals(method.Name, previousClrNameUse, StringComparison.Ordinal)) - { - Requires.Fail(Resources.ConflictingMethodAttributeValue, method.Name, previousClrNameUse, requestName); - } - } - else - { - requestMethodToClrMethodNameMap.Add(requestName, method.Name); - } - - JsonRpcMethodAttribute? attribute = mapping.FindMethodAttribute(method); - - if (attribute is null && (key.UseSingleObjectParameterDeserialization || key.ClientRequiresNamedArguments)) - { - attribute = new JsonRpcMethodAttribute(null) - { - UseSingleObjectParameterDeserialization = key.UseSingleObjectParameterDeserialization, - ClientRequiresNamedArguments = key.ClientRequiresNamedArguments, - }; - } - - // Skip this method if its signature matches one from a derived type we have already scanned. - MethodSignature methodTarget = new MethodSignature(method, attribute); - if (methodList.Contains(methodTarget)) - { - continue; - } - - methodList.Add(methodTarget); - - // If no explicit attribute has been applied, and the method ends with Async, - // register a request method name that does not include Async as well. - if (attribute?.Name is null && method.Name.EndsWith(ImpliedMethodNameAsyncSuffix, StringComparison.Ordinal)) - { - string nonAsyncMethodName = method.Name.Substring(0, method.Name.Length - ImpliedMethodNameAsyncSuffix.Length); - if (!candidateAliases.ContainsKey(nonAsyncMethodName)) - { - candidateAliases.Add(nonAsyncMethodName, method.Name); - } - } - } - } - - // Now that all methods have been discovered, add the candidate aliases - // if it would not introduce any collisions. - foreach (KeyValuePair candidateAlias in candidateAliases) - { - if (!requestMethodToClrMethodNameMap.ContainsKey(candidateAlias.Key)) - { - requestMethodToClrMethodNameMap.Add(candidateAlias.Key, candidateAlias.Value); - requestMethodToDelegateMap[candidateAlias.Key] = requestMethodToDelegateMap[candidateAlias.Value].ToList(); - } - } - - lock (RequestMethodToClrMethodMap) - { - if (RequestMethodToClrMethodMap.TryGetValue(key, out Dictionary>? lostRace)) - { - return lostRace; - } - - RequestMethodToClrMethodMap.Add(key, requestMethodToDelegateMap); - } - - return requestMethodToDelegateMap; - } - - /// - /// Given a type it will extract all events in the type hierarchy. It deals correctly with - /// interfaces. Note that it will return duplicates if they appear multiple times in the hierarchy. - /// - /// Type to reflect over and analyze its events. - /// A list of EventInfos found. -#if !NET10_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2065:UnrecognizedReflectionPattern", Justification = "false positive: https://github.com/dotnet/linker/issues/1731")] -#endif - private static IReadOnlyList GetEventInfos([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo exposedMembersOnType) - { - List eventInfos = new List(); - - foreach (EventInfo evt in exposedMembersOnType.GetEvents(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (evt.AddMethod is object && evt.AddMethod.IsPublic && !evt.AddMethod.IsStatic) - { - eventInfos.Add(evt); - } - } - - if (exposedMembersOnType.IsInterface) - { - Type[] interfaces = exposedMembersOnType.GetInterfaces(); - for (int i = 0; i < interfaces.Length; i++) - { - foreach (EventInfo evt in interfaces[i].GetTypeInfo().DeclaredEvents) - { - if (evt.AddMethod is object && !evt.AddMethod.IsStatic) - { - eventInfos.Add(evt); - } - } - } - } - - return eventInfos; - } - /// /// Adds a new RPC interface to an existing target registering RPC methods. /// - /// The methods to bers of the object. + /// A description of the members on to be mapped in as RPC targets. /// Target to invoke when incoming messages are received. /// A set of customizations for how the target object is registered. If , default options will be used. /// An optional object that may be disposed of to revert the addition of the target object. - private void AddRpcInterfaceToTarget(IReadOnlyDictionary> mapping, object target, JsonRpcTargetOptions options, RevertAddLocalRpcTarget? revertAddLocalRpcTarget) + internal void AddRpcInterfaceToTarget(RpcTargetMetadata targetType, object target, JsonRpcTargetOptions options, RevertAddLocalRpcTarget? revertAddLocalRpcTarget) { + JsonRpcMethodAttribute? pseudoAttribute = (options.ClientRequiresNamedArguments || options.UseSingleObjectParameterDeserialization) + ? new() { ClientRequiresNamedArguments = options.ClientRequiresNamedArguments, UseSingleObjectParameterDeserialization = options.UseSingleObjectParameterDeserialization } + : null; + lock (this.SyncObject) { - foreach (KeyValuePair> item in mapping) + foreach (KeyValuePair> item in targetType.Methods) { string rpcMethodName = options.MethodNameTransform is not null ? options.MethodNameTransform(item.Key) : item.Key; Requires.Argument(rpcMethodName is not null, nameof(options), nameof(JsonRpcTargetOptions.MethodNameTransform) + " delegate returned a value that is not a legal RPC method name."); @@ -549,12 +295,12 @@ private void AddRpcInterfaceToTarget(IReadOnlyDictionary e.Equals(newMethod))) { - var signatureAndTarget = new MethodSignatureAndTarget(newMethod, target, null); + var signatureAndTarget = new MethodSignatureAndTarget(newMethod, target, pseudoAttribute, null); this.TraceLocalMethodAdded(rpcMethodName, signatureAndTarget); revertAddLocalRpcTarget?.RecordMethodAdded(rpcMethodName, signatureAndTarget); existingList!.Add(signatureAndTarget); @@ -575,111 +321,14 @@ private void TraceLocalMethodAdded(string rpcMethodName, MethodSignatureAndTarge { Requires.NotNullOrEmpty(rpcMethodName, nameof(rpcMethodName)); - if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Information)) - { - this.TraceSource.TraceEvent(TraceEventType.Information, (int)JsonRpc.TraceEvents.LocalMethodAdded, "Added local RPC method \"{0}\" -> {1}", rpcMethodName, targetMethod); - } - } - - internal class MethodNameMap - { - private readonly ReadOnlyMemory interfaceMaps; - private readonly Dictionary methodAttributes = new Dictionary(); - private readonly Dictionary ignoreAttributes = new Dictionary(); - - [UnconditionalSuppressMessage("Trimming", "IL2111:UnrecognizedReflectionPattern", Justification = "typeInfo is annotated as preserve All members, so any Types returned from ImplementedInterfaces should be preserved as well. See https://github.com/dotnet/linker/issues/1731.")] - internal MethodNameMap([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo typeInfo) - { - Requires.NotNull(typeInfo, nameof(typeInfo)); - this.interfaceMaps = typeInfo.IsInterface ? default - : typeInfo.ImplementedInterfaces.Select(typeInfo.GetInterfaceMap).ToArray(); - } - - internal string GetRpcMethodName(MethodInfo method) - { - Requires.NotNull(method, nameof(method)); - - return this.FindMethodAttribute(method)?.Name ?? method.Name; - } - - /// - /// Get the , which may appear on the method itself or the interface definition of the method where applicable. - /// - /// The method to search for the attribute. - /// The attribute, if found. - internal JsonRpcMethodAttribute? FindMethodAttribute(MethodInfo method) - { - Requires.NotNull(method, nameof(method)); - - JsonRpcMethodAttribute? attribute; - lock (this.methodAttributes) - { - if (this.methodAttributes.TryGetValue(method, out attribute)) - { - return attribute; - } - } - - attribute = (JsonRpcMethodAttribute?)method.GetCustomAttribute(typeof(JsonRpcMethodAttribute)) - ?? (JsonRpcMethodAttribute?)this.FindMethodOnInterface(method)?.GetCustomAttribute(typeof(JsonRpcMethodAttribute)); - - lock (this.methodAttributes) - { - this.methodAttributes[method] = attribute; - } - - return attribute; - } - - /// - /// Get the , which may appear on the method itself or the interface definition of the method where applicable. - /// - /// The method to search for the attribute. - /// The attribute, if found. - internal JsonRpcIgnoreAttribute? FindIgnoreAttribute(MethodInfo method) - { - Requires.NotNull(method, nameof(method)); - - JsonRpcIgnoreAttribute? attribute; - lock (this.ignoreAttributes) - { - if (this.ignoreAttributes.TryGetValue(method, out attribute)) - { - return attribute; - } - } - - attribute = (JsonRpcIgnoreAttribute?)method.GetCustomAttribute(typeof(JsonRpcIgnoreAttribute)) - ?? (JsonRpcIgnoreAttribute?)this.FindMethodOnInterface(method)?.GetCustomAttribute(typeof(JsonRpcIgnoreAttribute)); - - lock (this.ignoreAttributes) - { - this.ignoreAttributes[method] = attribute; - } - - return attribute; - } - - private MethodInfo? FindMethodOnInterface(MethodInfo methodImpl) + if (this.TraceSource.Switch.ShouldTrace(TraceEventType.Verbose)) { - Requires.NotNull(methodImpl, nameof(methodImpl)); - - for (int i = 0; i < this.interfaceMaps.Length; i++) - { - InterfaceMapping map = this.interfaceMaps.Span[i]; - int methodIndex = Array.IndexOf(map.TargetMethods, methodImpl); - if (methodIndex >= 0) - { - return map.InterfaceMethods[methodIndex]; - } - } - - return null; + this.TraceSource.TraceEvent(TraceEventType.Verbose, (int)JsonRpc.TraceEvents.LocalMethodAdded, "Added local RPC method \"{0}\" -> {1}", rpcMethodName, targetMethod); } } /// - /// A class whose disposal will revert certain effects of a prior call to . + /// A class whose disposal will revert certain effects of a prior call to . /// internal class RevertAddLocalRpcTarget : IDisposable { @@ -757,83 +406,33 @@ internal void RecordObjectToDispose(object target) private class EventReceiver : IDisposable { - private static readonly MethodInfo OnEventRaisedMethodInfo = typeof(EventReceiver).GetMethod(nameof(OnEventRaised), BindingFlags.Instance | BindingFlags.NonPublic)!; - private static readonly MethodInfo OnEventRaisedGenericMethodInfo = typeof(EventReceiver).GetMethod(nameof(OnEventRaisedGeneric), BindingFlags.Instance | BindingFlags.NonPublic)!; private readonly JsonRpc jsonRpc; private readonly object server; private readonly EventInfo eventInfo; private readonly Delegate registeredHandler; private readonly string rpcEventName; - [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] - internal EventReceiver(JsonRpc jsonRpc, object server, EventInfo eventInfo, JsonRpcTargetOptions options) + internal EventReceiver(JsonRpc jsonRpc, object server, RpcTargetMetadata.EventMetadata eventMetadata, JsonRpcTargetOptions options) { - Requires.NotNull(jsonRpc, nameof(jsonRpc)); - Requires.NotNull(server, nameof(server)); - Requires.NotNull(eventInfo, nameof(eventInfo)); + Requires.NotNull(jsonRpc); + Requires.NotNull(server); + Requires.NotNull(eventMetadata); options = options ?? JsonRpcTargetOptions.Default; this.jsonRpc = jsonRpc; this.server = server; - this.eventInfo = eventInfo; - - this.rpcEventName = options.EventNameTransform is not null ? options.EventNameTransform(eventInfo.Name) : eventInfo.Name; + this.eventInfo = eventMetadata.Event; - try - { - // This might throw if our EventHandler-modeled method doesn't "fit" the event delegate signature. - // It will work for EventHandler and EventHandler, at least. - // If we want to support more, we'll likely have to use lightweight code-gen to generate a method - // with the right signature. - [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "False positive: https://github.com/dotnet/runtime/issues/114113")] - static ParameterInfo[] GetParameters(EventInfo eventInfo) => - eventInfo.EventHandlerType!.GetTypeInfo().GetMethod("Invoke")!.GetParameters(); - - ParameterInfo[] eventHandlerParameters = GetParameters(eventInfo); - if (eventHandlerParameters.Length != 2) - { - throw new NotSupportedException($"Unsupported event handler type for: \"{eventInfo.Name}\". Expected 2 parameters but had {eventHandlerParameters.Length}."); - } + this.rpcEventName = options.EventNameTransform is not null ? options.EventNameTransform(eventMetadata.Name) : eventMetadata.Name; - Type argsType = eventHandlerParameters[1].ParameterType; - if (typeof(EventArgs).GetTypeInfo().IsAssignableFrom(argsType)) - { - this.registeredHandler = OnEventRaisedMethodInfo.CreateDelegate(eventInfo.EventHandlerType!, this); - } - else - { - [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic method we construct has no dynamic member access requirements.")] - static MethodInfo GetOnEventRaisedClosedGenericMethod(Type argsType) - { - return OnEventRaisedGenericMethodInfo.MakeGenericMethod(argsType); - } - - MethodInfo closedGenericMethod = GetOnEventRaisedClosedGenericMethod(argsType); - this.registeredHandler = closedGenericMethod.CreateDelegate(eventInfo.EventHandlerType!, this); - } - } - catch (ArgumentException ex) - { - throw new NotSupportedException("Unsupported event handler type for: " + eventInfo.Name, ex); - } - - eventInfo.AddEventHandler(server, this.registeredHandler); + this.registeredHandler = eventMetadata.CreateEventHandler(jsonRpc, this.rpcEventName); + eventMetadata.Event.AddEventHandler(server, this.registeredHandler); } public void Dispose() { this.eventInfo.RemoveEventHandler(this.server, this.registeredHandler); } - - private void OnEventRaisedGeneric(object? sender, T args) - { - this.jsonRpc.NotifyAsync(this.rpcEventName, arguments: new object?[] { args }, argumentDeclaredTypes: new Type[] { typeof(T) }).Forget(); - } - - private void OnEventRaised(object? sender, EventArgs args) - { - this.jsonRpc.NotifyAsync(this.rpcEventName, new object[] { args }).Forget(); - } } } diff --git a/src/StreamJsonRpc/Reflection/TargetMethod.cs b/src/StreamJsonRpc/Reflection/TargetMethod.cs index fecc56d4c..50522f82b 100644 --- a/src/StreamJsonRpc/Reflection/TargetMethod.cs +++ b/src/StreamJsonRpc/Reflection/TargetMethod.cs @@ -15,7 +15,7 @@ public sealed class TargetMethod { private readonly JsonRpcRequest request; private readonly object? target; - private readonly MethodSignature? signature; + private readonly RpcTargetMetadata.TargetMethodMetadata? signature; private readonly object?[]? arguments; private SynchronizationContext? synchronizationContext; @@ -38,7 +38,7 @@ internal TargetMethod( List? argumentDeserializationExceptions = null; foreach (MethodSignatureAndTarget candidateMethod in candidateMethodTargets) { - int parameterCount = candidateMethod.Signature.Parameters.Length; + int parameterCount = candidateMethod.Signature.Parameters.Count; object?[] argumentArray = pool.Rent(parameterCount); try { @@ -82,7 +82,7 @@ internal TargetMethod( /// /// Gets the that will be invoked to handle the request, if one was found. /// - public MethodInfo? TargetMethodInfo => this.signature?.MethodInfo; + public MethodInfo? TargetMethodInfo => this.signature?.Method; /// /// Gets all the exceptions thrown while trying to deserialize arguments to candidate parameter types. @@ -107,12 +107,12 @@ internal string LookupErrorMessage } } - internal Type? ReturnType => this.signature?.MethodInfo.ReturnType; + internal Type? ReturnType => this.signature?.Method.ReturnType; /// public override string ToString() { - return this.signature is not null ? $"{this.signature.MethodInfo.DeclaringType!.FullName}.{this.signature.Name}({this.GetParameterSignature()})" : ""; + return this.signature is not null ? $"{this.signature.Method.DeclaringType!.FullName}.{this.signature.Name}({this.GetParameterSignature()})" : ""; } internal async Task InvokeAsync(CancellationToken cancellationToken) @@ -130,7 +130,7 @@ public override string ToString() Assumes.NotNull(this.synchronizationContext); await this.synchronizationContext; - return this.signature.MethodInfo.Invoke(!this.signature.MethodInfo.IsStatic ? this.target : null, this.arguments); + return this.signature.Method.Invoke(!this.signature.Method.IsStatic ? this.target : null, this.arguments); } private string? GetParameterSignature() => this.signature is not null ? string.Join(", ", this.signature.Parameters.Select(p => p.ParameterType.Name)) : null; @@ -145,11 +145,11 @@ private void AddErrorMessage(string message) this.errorMessages.Add(message); } - private bool TryGetArguments(JsonRpcRequest request, MethodSignature method, Span arguments) + private bool TryGetArguments(JsonRpcRequest request, RpcTargetMetadata.TargetMethodMetadata method, Span arguments) { Requires.NotNull(request, nameof(request)); Requires.NotNull(method, nameof(method)); - Requires.Argument(arguments.Length == method.Parameters.Length, nameof(arguments), "Length must equal number of parameters in method signature."); + Requires.Argument(arguments.Length == method.Parameters.Count, nameof(arguments), "Length must equal number of parameters in method signature."); // ref and out parameters aren't supported. if (method.HasOutOrRefParameters) @@ -159,7 +159,7 @@ private bool TryGetArguments(JsonRpcRequest request, MethodSignature method, Spa } // When there is a CancellationToken parameter, we require that it always be the last parameter. - ReadOnlySpan methodParametersExcludingCancellationToken = new(method.Parameters, 0, method.TotalParamCountExcludingCancellationToken); + ReadOnlySpan methodParametersExcludingCancellationToken = method.ParametersMemory.Span[..method.TotalParamCountExcludingCancellationToken]; Span argumentsExcludingCancellationToken = arguments.Slice(0, method.TotalParamCountExcludingCancellationToken); if (method.HasCancellationTokenParameter) { diff --git a/src/StreamJsonRpc/Resources.resx b/src/StreamJsonRpc/Resources.resx index a5e41c68f..08f4ffd7e 100644 --- a/src/StreamJsonRpc/Resources.resx +++ b/src/StreamJsonRpc/Resources.resx @@ -1,17 +1,17 @@  - @@ -138,14 +138,6 @@ "{0}" is not an interface. - - .NET methods '{0}' and '{1}' cannot both map to the same request method name: '{2}'. - {0} is the first method name, {1} is the second method name, {2} is the attribute property value. - - - All overloads and overrides of the '{0}' method must share a common value for {1}.{2}. - {0} is the method name, {1} is the attribute name, {2} is the attribute property name. - A method with the same name and equivalent parameters has already been registered. @@ -389,4 +381,4 @@ This operation can only be performed once on this object. - + \ No newline at end of file diff --git a/src/StreamJsonRpc/RpcTargetMetadata.cs b/src/StreamJsonRpc/RpcTargetMetadata.cs new file mode 100644 index 000000000..f1436d35f --- /dev/null +++ b/src/StreamJsonRpc/RpcTargetMetadata.cs @@ -0,0 +1,861 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using Microsoft.VisualStudio.Threading; + +namespace StreamJsonRpc; + +/// +/// Describes an RPC target type, which can be an interface or a class. +/// +public class RpcTargetMetadata +{ + private const string ImpliedMethodNameAsyncSuffix = "Async"; + private static readonly ConcurrentDictionary EventHandlerFactories = []; + private static readonly ConcurrentDictionary Interfaces = []; + private static readonly ConcurrentDictionary PublicClass = []; + private static readonly ConcurrentDictionary NonPublicClass = []; + private static readonly MethodInfo RegisterEventArgsMethodInfo = typeof(RpcTargetMetadata).GetMethod(nameof(RegisterEventArgs), BindingFlags.Public | BindingFlags.Static) ?? throw Assumes.NotReachable(); + private static Action? dynamicEventHandlerFactoryRegistration; + + /// + /// Represents a method that creates a delegate to handle a specified JSON-RPC event. + /// + /// The JSON-RPC connection for which the event handler delegate is being created. Cannot be . + /// The name of the event for which to create the handler delegate. Cannot be or empty. + /// A delegate instance that handles the specified event for the given JSON-RPC connection. + public delegate Delegate CreateEventHandlerDelegate(JsonRpc rpc, string eventName); + + private interface IEventHandlerFactory + { + /// + /// Creates an event handler for the specified event. + /// + /// The JSON-RPC instance to use for sending notifications. + /// The name of the event to create a handler for. + /// The type of the event/delegate to be returned. + /// A delegate that can be used as an event handler. + Delegate CreateEventHandler(JsonRpc rpc, string eventName, Type delegateType); + } + + /// + /// Gets the methods that can be invoked on this RPC target. + /// + public required IReadOnlyDictionary> Methods { get; init; } + + /// + /// Gets the list of events that can be raised by this RPC target. + /// + public required IReadOnlyList Events { get; init; } + + /// + /// Gets the type of the RPC target, which can be an interface or a class. + /// + public required Type TargetType { get; init; } + + /// + /// Enables dynamic generation of event handlers for delegates + /// where TEventArgs is a value type. + /// + /// + /// This method is not safe to use in NativeAOT applications. + /// Such applications should either call directly for each value-type type argument, + /// or rely on source generation to do so. + /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic method we construct has no dynamic member access requirements.")] + public static void EnableDynamicEventHandlerCreation() + { + dynamicEventHandlerFactoryRegistration ??= (type) => + { + RegisterEventArgsMethodInfo.MakeGenericMethod(type).Invoke(null, null); + }; + } + + /// + /// Creates an instance of RpcTargetMetadata that describes the specified RPC contract interface. + /// + /// The interface type that defines the RPC contract. Must not be null and must represent an interface type. + /// An instance that provides metadata for the specified RPC contract interface. + /// + /// + /// If metadata for the specified interface has already been created, the existing instance is returned. + /// Otherwise, a new metadata instance is generated. + /// This method is typically used to obtain metadata required for dispatching or proxying RPC calls based + /// on an interface definition. + /// + /// + /// While convenient, this method produces the least trimmable code. + /// For a smaller trimmed application, use instead. + /// + /// + public static RpcTargetMetadata FromInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type rpcContract) + { + Requires.NotNull(rpcContract); + Requires.Argument(rpcContract.IsInterface, nameof(rpcContract), "The type must be an interface."); + + return Interfaces.TryGetValue(rpcContract, out RpcTargetMetadata? result) + ? result + : FromInterface(InterfaceCollection.Create(rpcContract)); + } + + /// + /// Creates metadata describing the RPC target for the specified set of interfaces. + /// + /// + /// A collection of interfaces, including the primary interface and any interfaces it derives from, to generate + /// metadata for. + /// + /// An instance of representing the RPC target metadata for the provided interfaces. + /// + /// + /// If metadata for the specified interface has already been created, the existing instance is returned. + /// Otherwise, a new metadata instance is generated. + /// This method is typically used to obtain metadata required for dispatching or proxying RPC calls based + /// on an interface definition. + /// + /// + /// Thrown if does not represent all the interfaces that the target interface derives from. + public static RpcTargetMetadata FromInterface(InterfaceCollection interfaces) + { + Requires.NotNull(interfaces); + IReadOnlyList missingInterfaces = interfaces.GetMissingInterfacesFromSet(); + Requires.Argument(missingInterfaces is [], nameof(interfaces), $"The interface collection is missing interfaces that the primary interface derives from: {string.Join(", ", missingInterfaces.Select(t => t.FullName))}."); + + if (Interfaces.TryGetValue(interfaces.PrimaryInterface, out RpcTargetMetadata? result)) + { + // If we already have metadata for the primary interface, return it. + return result; + } + + Builder builder = new(interfaces); + for (int i = 0; i < interfaces.Count; i++) + { + WalkInterface(interfaces[i]); + } + + void WalkInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type iface) + { + AddMethods(builder, iface.GetMethods(BindingFlags.Public | BindingFlags.Instance)); + AddEvents(builder, iface.GetEvents(BindingFlags.Public | BindingFlags.Instance)); + } + + result = builder.ToImmutable(); + + // It's safe to store and share the result because we confirmed that InterfaceCollection is complete, + // and the collection itself ensures that it does not have an excess of interfaces. + return Interfaces.TryAdd(interfaces.PrimaryInterface, result) ? result : Interfaces[interfaces.PrimaryInterface]; + } + + /// + /// Creates a new instance of for the specified class type, including all of its + /// RPC target members. + /// + /// The type representing the class for which to generate metadata. Must not be null and should be a concrete class + /// type. + /// An instance containing metadata for the specified class and its interfaces. + /// + /// + /// While convenient, this method produces the least trimmable code. + /// For a smaller trimmed application, use instead. + /// + /// + public static RpcTargetMetadata FromClass([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type classType) + => FromClass(classType, ClassAndInterfaces.Create(classType)); + + /// + /// Creates an instance of for the specified class type using the provided metadata. + /// All public methods and events will be exposed to RPC clients, + /// unless is applied to them. + /// + /// The class for which to generate metadata. Must be a non-null class type. + /// The metadata describing the class and its interfaces. Must not be null and must correspond to the specified + /// class type. + /// An instance representing the public methods and events of the specified class. + /// + /// If metadata for the specified class type has already been created, the existing instance is returned. + /// Otherwise, a new instance is generated. If all interfaces implemented by the + /// class are present in the provided metadata, the resulting instance will be cached for later reuse. + /// + /// + /// Thrown if the does not match the in the provided metadata. + /// + public static RpcTargetMetadata FromClass([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type classType, ClassAndInterfaces metadata) + { + Requires.NotNull(classType); + Requires.Argument(classType.IsClass, nameof(classType), "The type must be a class."); + Requires.NotNull(metadata); + Requires.Argument(classType == metadata.ClassType, nameof(metadata), "Metadata must describe the target class."); + + if (PublicClass.TryGetValue(classType, out RpcTargetMetadata? result)) + { + return result; + } + + Builder builder = new(metadata); + AddMethods(builder, classType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)); + AddEvents(builder, classType.GetEvents(BindingFlags.Public | BindingFlags.Instance)); + result = builder.ToImmutable(); + + // If the caller does not have a complete idea of all interfaces that the class implements, + // we can still return the result, but we will not cache it since that may pollute other users with + // more complete inputs. + IReadOnlyList missingInterfaces = metadata.GetMissingInterfacesFromSet(); + if (missingInterfaces is not []) + { + return result; + } + + return PublicClass.TryAdd(classType, result) ? result : PublicClass[classType]; + } + + /// + /// Creates an instance of RpcTargetMetadata for the specified class type, including non-public members + /// that are not attributed with . + /// + /// The type of the class for which to generate metadata. Must not be null. + /// A RpcTargetMetadata instance containing metadata for the specified class type, including its non-public members. + /// + /// + /// While convenient, this method produces the least trimmable code. + /// For a smaller trimmed application, use instead. + /// + /// + public static RpcTargetMetadata FromClassNonPublic([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type classType) + => FromClassNonPublic(classType, ClassAndInterfaces.Create(classType)); + + /// + /// Creates an instance of for the specified class type using the provided metadata. + /// All methods and events will be exposed to RPC clients, including non-public members, + /// unless is applied to them. + /// + /// The class for which to generate metadata. Must be a non-null class type. + /// The metadata describing the class and its interfaces. Must not be null and must correspond to the specified + /// class type. + /// An instance representing the public methods and events of the specified class. + /// + /// If metadata for the specified class type has already been created, the existing instance is returned. + /// Otherwise, a new instance is generated. If all interfaces implemented by the + /// class are present in the provided metadata, the resulting instance will be cached for later reuse. + /// + /// + /// Thrown if the does not match the in the provided metadata. + /// + public static RpcTargetMetadata FromClassNonPublic([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicEvents)] Type classType, ClassAndInterfaces metadata) + { + Requires.NotNull(classType); + Requires.Argument(classType.IsClass, nameof(classType), "The type must be a class."); + Requires.NotNull(metadata); + Requires.Argument(classType == metadata.ClassType, nameof(metadata), "Metadata must describe the target class."); + + if (NonPublicClass.TryGetValue(classType, out RpcTargetMetadata? result)) + { + return result; + } + + Builder builder = new(metadata); + AddMethods(builder, classType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)); + AddEvents(builder, classType.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); + result = builder.ToImmutable(); + + // If the caller does not have a complete idea of all interfaces that the class implements, + // we can still return the result, but we will not cache it since that may pollute other users with + // more complete inputs. + IReadOnlyList missingInterfaces = metadata.GetMissingInterfacesFromSet(); + if (missingInterfaces is not []) + { + return result; + } + + return NonPublicClass.TryAdd(classType, result) ? result : NonPublicClass[classType]; + } + + /// + /// Creates an event handler factory that supports for a given . + /// + /// + /// The type argument used in . + /// Only structs are supported because only value types need registration. Reference types work without registration. + /// + public static void RegisterEventArgs() + where TEventArgs : struct => EventHandlerFactories.TryAdd(typeof(TEventArgs), new EventHandlerFactory()); + + private static void AddMethods(Builder builder, IEnumerable methods) + { + foreach (MethodInfo method in methods) + { + TryAddCandidateMethod(builder, method); + } + } + + private static bool TryAddCandidateMethod(Builder builder, MethodInfo method) + { + if (method.IsSpecialName || method.IsConstructor || method.DeclaringType == typeof(object)) + { + return false; + } + + JsonRpcIgnoreAttribute? ignoreAttribute = FindMethodAttribute(builder, method); + JsonRpcMethodAttribute? methodAttribute = FindMethodAttribute(builder, method); + + if (ignoreAttribute is not null) + { + if (methodAttribute is not null) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.JsonRpcMethodAndIgnoreAttributesFound, method.Name)); + } + + return false; + } + + var methodMetadata = TargetMethodMetadata.From(method, methodAttribute); + + if (!builder.Methods.TryGetValue(methodMetadata.Name, out List? methodList)) + { + builder.Methods[methodMetadata.Name] = methodList = []; + } + + methodList.Add(methodMetadata); + return true; + } + + private static void AddEvents(Builder builder, IEnumerable events) + { + foreach (EventInfo @event in events) + { + TryAddCandidateEvent(builder, @event); + } + } + + private static bool TryAddCandidateEvent(Builder builder, EventInfo @event) + { + if (@event.EventHandlerType is null) + { + return false; + } + + CreateEventHandlerDelegate? createEventHandler; + if (@event.EventHandlerType == typeof(EventHandler)) + { + createEventHandler = (rpc, eventName) => new EventHandler((object? sender, EventArgs args) => rpc.NotifyAsync(eventName, [args]).Forget()); + } + else if (@event.EventHandlerType.IsGenericType && @event.EventHandlerType.GetGenericTypeDefinition() == typeof(EventHandler<>) && + @event.EventHandlerType.GetGenericArguments() is [{ } argType]) + { + createEventHandler = CreateEventDelegate(argType); + } + else if (GetParameters(@event) is [{ ParameterType: Type senderType }, { ParameterType: Type argType2 }] && senderType == typeof(object)) + { + createEventHandler = CreateEventDelegate(argType2); + } + else + { + // We don't support this delegate type. + throw new NotSupportedException($"Only EventHandler and EventHandler delegates are supported for RPC, but {@event.DeclaringType}.{@event.Name} has unsupported type {@event.EventHandlerType}."); + } + + builder.Events.Add(new EventMetadata + { + Event = @event, + Name = @event.Name, + EventHandlerType = @event.EventHandlerType, + CreateEventHandler = createEventHandler, + }); + return true; + + CreateEventHandlerDelegate CreateEventDelegate(Type argType) + { + if (!argType.IsValueType) + { + return (rpc, eventName) => + { + Type[] argTypes = [argType]; + Delegate d = (object? sender, object? args) => rpc.NotifyAsync(eventName, [args], [argType]).Forget(); + return Delegate.CreateDelegate(@event.EventHandlerType, d.Target, d.Method); + }; + } + else if (EventHandlerFactories.TryGetValue(argType, out IEventHandlerFactory? factory)) + { + return (jsonRpc, eventName) => factory.CreateEventHandler(jsonRpc, eventName, @event.EventHandlerType); + } + else + { + if (dynamicEventHandlerFactoryRegistration is not null) + { + dynamicEventHandlerFactoryRegistration(argType); + Assumes.True(EventHandlerFactories.TryGetValue(argType, out factory)); + return (jsonRpc, eventName) => factory.CreateEventHandler(jsonRpc, eventName, @event.EventHandlerType); + } + + // We don't have a factory registered for this value type. + throw new NotSupportedException($"{@event.DeclaringType}.{@event.Name} event uses {argType} as its second parameter. Structs used as event args must be registered beforehand using {nameof(RpcTargetMetadata)}.{nameof(RegisterEventArgs)}()."); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "False positive: https://github.com/dotnet/runtime/issues/114113")] + static ParameterInfo[] GetParameters(EventInfo eventInfo) => + eventInfo.EventHandlerType!.GetTypeInfo().GetMethod("Invoke")!.GetParameters(); + } + + private static T? FindMethodAttribute(Builder builder, MethodInfo method) + where T : Attribute + { + if (method.GetCustomAttribute() is T attribute) + { + return attribute; + } + + for (int i = 0; i < builder.InterfaceMaps.Length; i++) + { + InterfaceMapping map = builder.InterfaceMaps.Span[i]; + int methodIndex = Array.IndexOf(map.TargetMethods, method); + if (methodIndex >= 0 && map.InterfaceMethods[methodIndex].GetCustomAttribute() is T inheritedAttribute) + { + return inheritedAttribute; + } + } + + return null; + } + + private static IReadOnlyList GetMissingInterfacesFromSet([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type targetType, IReadOnlyList interfaces, int startIndex) + { + List? missing = null; + + // Verify that all interfaces are present. + foreach (Type derivedFrom in targetType.GetInterfaces()) + { + bool found = false; + for (int i = startIndex; i < interfaces.Count; i++) + { + if (interfaces[i].Interface == derivedFrom) + { + found = true; + break; + } + } + + if (!found) + { + missing ??= []; + missing.Add(derivedFrom); + } + } + + return missing ?? []; + } + + internal struct RpcTargetInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type iface) + { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] + public Type Interface => iface; + } + + /// + /// Represents a collection of interfaces implemented by a specified class type for use as an RPC target. + /// + /// + /// Use this class to track and manage the interfaces that a given class type implements, + /// typically for remote procedure call (RPC) scenarios. Interfaces must be added explicitly after construction + /// using the Add method, unless the Create factory method is used to automatically populate the collection. + /// + public class ClassAndInterfaces + { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + private readonly Type classType; + + private readonly List interfaces = []; + + /// + /// Initializes a new instance of the class. + /// + /// The class type serving as the RPC target. + /// + /// + /// After construction, all interfaces that the implements + /// must be added using the method. + /// + /// + /// Use the factory method to automate full initialization of this collection, + /// at the cost of a less trimmable application. + /// + /// + public ClassAndInterfaces([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type classType) + { + Requires.NotNull(classType); + Requires.Argument(classType.IsClass, nameof(classType), "The type must be a class."); + + this.classType = classType; + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + internal Type ClassType => this.classType; + + internal int InterfaceCount => this.interfaces.Count; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicMethods)] + internal Type this[int index] => this.interfaces[index].Interface; + + /// + /// Creates a new instance of the class that represents the specified class type and all of + /// its implemented interfaces. + /// + /// The Type object representing the class to include, along with all interfaces implemented by the class. Must + /// not be null. + /// A instance containing the specified class type and all interfaces it implements. + [SuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "We use the All link demand on rpcContract, so results of GetInterfaces() should work. See https://github.com/dotnet/linker/issues/1731")] + [UnconditionalSuppressMessage("Trimming", "IL2062", Justification = "We use the All link demand on rpcContract, so results of GetInterfaces() should work. See https://github.com/dotnet/linker/issues/1731")] + public static ClassAndInterfaces Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type classType) + { + ClassAndInterfaces result = new(classType); + foreach (Type iface in classType.GetInterfaces()) + { + result.Add(iface); + } + + return result; + } + + /// + /// Adds an interface to the set of interfaces supported by the RPC target. + /// + /// The interface type to add. Must be an interface implemented by the target class. + public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type iface) + { + Requires.NotNull(iface); + Requires.Argument(iface.IsInterface && iface.IsAssignableFrom(this.classType), nameof(iface), "This type must be an interface that the class implements."); + + this.interfaces.Add(new RpcTargetInterface(iface)); + } + + internal IReadOnlyList GetMissingInterfacesFromSet() => RpcTargetMetadata.GetMissingInterfacesFromSet(this.classType, this.interfaces, 0); + } + + /// + /// Represents a collection of interface types associated with a primary interface for an RPC target. + /// Provides enumeration and management of the primary interface and its base interfaces. + /// + /// + /// This class is typically used to track and expose the set of interfaces implemented by an RPC target, + /// ensuring that all relevant contract interfaces are available for reflection or invocation scenarios. + /// + public class InterfaceCollection : IEnumerable + { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.Interfaces)] + private Type primaryInterface; + + private List interfaces; + + /// + /// Initializes a new instance of the class. + /// + /// The primary RPC target interface. + /// + /// + /// After construction, all interfaces that the derives from + /// must be added using the method. + /// + /// + /// Use the factory method to automate full initialization of this collection, + /// at the cost of a less trimmable application. + /// + /// + public InterfaceCollection([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.Interfaces)] Type primaryInterface) + { + Requires.NotNull(primaryInterface); + Requires.Argument(primaryInterface.IsInterface, nameof(primaryInterface), "The type must be an interface."); + + this.primaryInterface = primaryInterface; + this.interfaces = [new(primaryInterface)]; + } + + /// + /// Gets the number of interfaces in the collection, including the primary interface. + /// + internal int Count => this.interfaces.Count; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.Interfaces)] + internal Type PrimaryInterface => this.primaryInterface; + + /// + /// Gets the interface type at the specified index in the collection. + /// + /// The zero-based index of the interface to retrieve. The zero-index interface is always the . + /// The representing the interface at the specified index. + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] + internal Type this[int index] => this.interfaces[index].Interface; + + /// + /// Adds an interface to the set of base interfaces for this RPC target. + /// + /// The interface type to add. Must be an interface from which the primary interface derives. Cannot be null. + public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicEvents)] Type iface) + { + Requires.NotNull(iface); + Requires.Argument(iface.IsInterface && iface.IsAssignableFrom(this.interfaces[0].Interface), nameof(iface), "This type must be an interface from which the primary interface derives."); + + this.interfaces.Add(new RpcTargetInterface(iface)); + } + + /// + public IEnumerator GetEnumerator() => this.interfaces.Select(t => t.Interface).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + [SuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "We use the All link demand on rpcContract, so results of GetInterfaces() should work. See https://github.com/dotnet/linker/issues/1731")] + [UnconditionalSuppressMessage("Trimming", "IL2062", Justification = "We use the All link demand on rpcContract, so results of GetInterfaces() should work. See https://github.com/dotnet/linker/issues/1731")] + internal static InterfaceCollection Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type primaryInterface) + { + InterfaceCollection result = new(primaryInterface); + foreach (Type iface in primaryInterface.GetInterfaces()) + { + result.Add(iface); + } + + return result; + } + + internal IReadOnlyList GetMissingInterfacesFromSet() => RpcTargetMetadata.GetMissingInterfacesFromSet(this.primaryInterface, this.interfaces, 1); + } + + /// + /// Provides metadata describing an RPC event. + /// + /// + /// + /// Use this class to access information about an event and its handler in scenarios where events + /// are invoked via RPC mechanisms. The metadata includes the event's reflection information, the name of the RPC + /// method to invoke, the expected delegate type for the handler, and a factory for creating handler delegates. This + /// class is typically used in frameworks or infrastructure that dynamically manage event subscriptions and + /// invocations. + /// + /// + /// Instances of this class are generally constructed internally and are not intended to be created + /// directly by consumers. + /// + /// + public class EventMetadata + { + /// + /// Gets the event for which this metadata is describing the handler. + /// + public required EventInfo Event { get; init; } + + /// + /// Gets the name of the RPC method that this event will invoke when raised. + /// + public required string Name { get; init; } + + /// + /// Gets the delegate type of the event handler. + /// + public required Type EventHandlerType { get; init; } + + /// + /// Gets a factory method that creates a delegate to handle the event. + /// + public required CreateEventHandlerDelegate CreateEventHandler { get; init; } + } + + /// + /// Represents metadata about an RPC target method. + /// + /// + /// + /// This class is typically used to encapsulate information about a method that can be invoked + /// via JSON-RPC. It provides access to the method's reflection data and any custom attributes relevant to JSON-RPC + /// dispatch. + /// + /// + /// Instances of this class are generally constructed internally and are not intended to be created + /// directly by consumers. + /// + /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] + public class TargetMethodMetadata + { + private ParameterInfo[]? parameters; + + /// + /// Gets the for the RPC target method. + /// + public required MethodInfo Method { get; init; } + + /// + /// Gets the RPC target name that should invoke this method. + /// + public required string Name { get; init; } + + /// + /// Gets the that applies to this method, if any. + /// + public required JsonRpcMethodAttribute? Attribute { get; init; } + + /// + /// Gets the parameters on the method. + /// + /// + /// This is equivalent to , but cached for performance. + /// + internal IReadOnlyList Parameters => this.parameters ??= this.Method.GetParameters() ?? []; + + /// + /// Gets a view of the parameters on the method. + /// + /// + internal ReadOnlyMemory ParametersMemory => (ParameterInfo[])this.Parameters; + + /// + /// Gets a value indicating whether the method is declared as public. + /// + internal bool IsPublic => this.Method.IsPublic; + + internal int RequiredParamCount => this.Parameters.Count(pi => !pi.IsOptional && pi.ParameterType != typeof(CancellationToken)); + + internal int TotalParamCountExcludingCancellationToken => this.HasCancellationTokenParameter ? this.Parameters.Count - 1 : this.Parameters.Count; + + internal bool HasCancellationTokenParameter => this.Parameters is [.., { ParameterType: { } type }] && type == typeof(CancellationToken); + + internal bool HasOutOrRefParameters => this.Parameters.Any(pi => pi.IsOut || pi.ParameterType.IsByRef); + + [ExcludeFromCodeCoverage] + private string DebuggerDisplay => $"{this.Method.DeclaringType}.{this.Name}({string.Join(", ", this.Parameters.Select(p => p.ParameterType.Name))})"; + + /// + public override string ToString() => this.DebuggerDisplay; + + internal static TargetMethodMetadata From(MethodInfo method, JsonRpcMethodAttribute? attribute) + => new() + { + Method = method, + Name = attribute?.Name ?? method.Name, + Attribute = attribute, + }; + + internal bool EqualSignature(TargetMethodMetadata other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + if (this.TotalParamCountExcludingCancellationToken != other.TotalParamCountExcludingCancellationToken) + { + return false; + } + + for (int i = 0; i < this.TotalParamCountExcludingCancellationToken; i++) + { + if (this.Parameters[i].ParameterType != other.Parameters[i].ParameterType) + { + return false; + } + } + + return true; + } + + internal bool MatchesParametersExcludingCancellationToken(ReadOnlySpan parameters) + { + if (this.TotalParamCountExcludingCancellationToken == parameters.Length) + { + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].ParameterType != this.Parameters[i].ParameterType) + { + return false; + } + } + + return true; + } + + return false; + } + } + + private class EventHandlerFactory : IEventHandlerFactory + { + public Delegate CreateEventHandler(JsonRpc rpc, string eventName, Type delegateType) + { + Type[] argTypes = [typeof(TEventArgs)]; + Delegate d = (object? sender, TEventArgs args) => rpc.NotifyAsync(eventName, [args], argTypes).Forget(); + return d.Method.CreateDelegate(delegateType, d.Target); + } + } + + private class Builder + { + internal Builder(InterfaceCollection interfaces) + { + this.TargetType = interfaces.PrimaryInterface; + } + + internal Builder(ClassAndInterfaces classAndInterfaces) + { + this.TargetType = classAndInterfaces.ClassType; + + InterfaceMapping[] mapping = new InterfaceMapping[classAndInterfaces.InterfaceCount]; + for (int i = 0; i < classAndInterfaces.InterfaceCount; i++) + { + mapping[i] = this.TargetType.GetTypeInfo().GetInterfaceMap(classAndInterfaces[i]); + } + + this.InterfaceMaps = mapping; + } + + internal ReadOnlyMemory InterfaceMaps { get; } + + internal Type TargetType { get; } + + internal Dictionary> Methods { get; } = new(StringComparer.Ordinal); + + internal List Events { get; } = []; + + internal RpcTargetMetadata ToImmutable() + { + this.GenerateAliases(); + + return new RpcTargetMetadata + { + TargetType = this.TargetType, + Methods = this.Methods.ToImmutableDictionary(kv => kv.Key, kv => (IReadOnlyList)kv.Value.ToArray()), + Events = [.. this.Events], + }; + } + + private void GenerateAliases() + { + // Create aliases for methods ending in Async that don't have the JsonRpcMethodAttribute, + // when renaming them would not create overload collisions with the shortened name. + Dictionary> aliasedMethods = []; + foreach ((string name, List overloads) in this.Methods) + { + if (name.EndsWith(ImpliedMethodNameAsyncSuffix, StringComparison.Ordinal)) + { + string alias = name[..^ImpliedMethodNameAsyncSuffix.Length]; + if (!this.Methods.ContainsKey(alias)) + { + List implicitlyNamed = [.. overloads.Where(o => o.Attribute?.Name is null)]; + if (implicitlyNamed.Count > 0) + { + aliasedMethods.Add(alias, [.. overloads.Where(o => o.Attribute?.Name is null)]); + } + } + } + } + + foreach ((string alias, List overloads) in aliasedMethods) + { + this.Methods.Add(alias, overloads); + } + } + } +} diff --git a/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt index eacf88869..432de8d70 100644 --- a/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/net8.0/PublicAPI.Unshipped.txt @@ -1,13 +1,24 @@ +override StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.ToString() -> string! static StreamJsonRpc.JsonRpcProxyOptions.Default.get -> StreamJsonRpc.JsonRpcProxyOptions! static StreamJsonRpc.NamedArgs.Create(System.Type! objectType, object? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.NamedArgs.Create(T? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.Reflection.ProxyBase.CreateProxy(StreamJsonRpc.JsonRpc! jsonRpc, in StreamJsonRpc.Reflection.ProxyInputs proxyInputs, bool startOrFail) -> StreamJsonRpc.IJsonRpcClientProxy! static StreamJsonRpc.Reflection.ProxyBase.TryCreateProxy(StreamJsonRpc.JsonRpc! jsonRpc, in StreamJsonRpc.Reflection.ProxyInputs proxyInputs, out StreamJsonRpc.IJsonRpcClientProxy? proxy) -> bool +static StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.Create(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! +static StreamJsonRpc.RpcTargetMetadata.EnableDynamicEventHandlerCreation() -> void +static StreamJsonRpc.RpcTargetMetadata.FromClass(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClass(System.Type! classType, StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! metadata) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClassNonPublic(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClassNonPublic(System.Type! classType, StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! metadata) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromInterface(StreamJsonRpc.RpcTargetMetadata.InterfaceCollection! interfaces) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromInterface(System.Type! rpcContract) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.RegisterEventArgs() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute StreamJsonRpc.ExportRpcContractProxiesAttribute.ExportRpcContractProxiesAttribute() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.get -> bool StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.set -> void StreamJsonRpc.IJsonRpcClientProxy.As() -> T? +StreamJsonRpc.JsonRpc.AddLocalRpcTarget(StreamJsonRpc.RpcTargetMetadata! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.GetJsonSchema(Nerdbank.MessagePack.JsonSchemaContext! context, PolyType.Abstractions.ITypeShape! typeShape) -> System.Text.Json.Nodes.JsonObject? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Read(ref Nerdbank.MessagePack.MessagePackReader reader, Nerdbank.MessagePack.SerializationContext context) -> System.Collections.Generic.IAsyncEnumerable? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Write(ref Nerdbank.MessagePack.MessagePackWriter writer, in System.Collections.Generic.IAsyncEnumerable? value, Nerdbank.MessagePack.SerializationContext context) -> void @@ -80,6 +91,41 @@ StreamJsonRpc.Reflection.ProxyInputs.ContractInterface.init -> void StreamJsonRpc.Reflection.ProxyInputs.Options.get -> StreamJsonRpc.JsonRpcProxyOptions? StreamJsonRpc.Reflection.ProxyInputs.Options.init -> void StreamJsonRpc.Reflection.ProxyInputs.ProxyInputs() -> void +StreamJsonRpc.RpcTargetMetadata +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.Add(System.Type! iface) -> void +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.ClassAndInterfaces(System.Type! classType) -> void +StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate +StreamJsonRpc.RpcTargetMetadata.EventMetadata +StreamJsonRpc.RpcTargetMetadata.EventMetadata.CreateEventHandler.get -> StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.CreateEventHandler.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Event.get -> System.Reflection.EventInfo! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Event.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventHandlerType.get -> System.Type! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventHandlerType.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Name.get -> string! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Name.init -> void +StreamJsonRpc.RpcTargetMetadata.Events.get -> System.Collections.Generic.IReadOnlyList! +StreamJsonRpc.RpcTargetMetadata.Events.init -> void +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.Add(System.Type! iface) -> void +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.InterfaceCollection(System.Type! primaryInterface) -> void +StreamJsonRpc.RpcTargetMetadata.Methods.get -> System.Collections.Generic.IReadOnlyDictionary!>! +StreamJsonRpc.RpcTargetMetadata.Methods.init -> void +StreamJsonRpc.RpcTargetMetadata.RpcTargetMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.get -> StreamJsonRpc.JsonRpcMethodAttribute? +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Method.get -> System.Reflection.MethodInfo! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Method.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.get -> string! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.TargetMethodMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.TargetType.get -> System.Type! +StreamJsonRpc.RpcTargetMetadata.TargetType.init -> void +virtual StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate.Invoke(StreamJsonRpc.JsonRpc! rpc, string! eventName) -> System.Delegate! StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy.ProgressProxy(StreamJsonRpc.JsonRpc! rpc, object! token, bool useNamedArguments) -> void StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy.Report(T value) -> void diff --git a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt index eacf88869..432de8d70 100644 --- a/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,13 +1,24 @@ +override StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.ToString() -> string! static StreamJsonRpc.JsonRpcProxyOptions.Default.get -> StreamJsonRpc.JsonRpcProxyOptions! static StreamJsonRpc.NamedArgs.Create(System.Type! objectType, object? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.NamedArgs.Create(T? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.Reflection.ProxyBase.CreateProxy(StreamJsonRpc.JsonRpc! jsonRpc, in StreamJsonRpc.Reflection.ProxyInputs proxyInputs, bool startOrFail) -> StreamJsonRpc.IJsonRpcClientProxy! static StreamJsonRpc.Reflection.ProxyBase.TryCreateProxy(StreamJsonRpc.JsonRpc! jsonRpc, in StreamJsonRpc.Reflection.ProxyInputs proxyInputs, out StreamJsonRpc.IJsonRpcClientProxy? proxy) -> bool +static StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.Create(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! +static StreamJsonRpc.RpcTargetMetadata.EnableDynamicEventHandlerCreation() -> void +static StreamJsonRpc.RpcTargetMetadata.FromClass(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClass(System.Type! classType, StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! metadata) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClassNonPublic(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClassNonPublic(System.Type! classType, StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! metadata) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromInterface(StreamJsonRpc.RpcTargetMetadata.InterfaceCollection! interfaces) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromInterface(System.Type! rpcContract) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.RegisterEventArgs() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute StreamJsonRpc.ExportRpcContractProxiesAttribute.ExportRpcContractProxiesAttribute() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.get -> bool StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.set -> void StreamJsonRpc.IJsonRpcClientProxy.As() -> T? +StreamJsonRpc.JsonRpc.AddLocalRpcTarget(StreamJsonRpc.RpcTargetMetadata! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.GetJsonSchema(Nerdbank.MessagePack.JsonSchemaContext! context, PolyType.Abstractions.ITypeShape! typeShape) -> System.Text.Json.Nodes.JsonObject? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Read(ref Nerdbank.MessagePack.MessagePackReader reader, Nerdbank.MessagePack.SerializationContext context) -> System.Collections.Generic.IAsyncEnumerable? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Write(ref Nerdbank.MessagePack.MessagePackWriter writer, in System.Collections.Generic.IAsyncEnumerable? value, Nerdbank.MessagePack.SerializationContext context) -> void @@ -80,6 +91,41 @@ StreamJsonRpc.Reflection.ProxyInputs.ContractInterface.init -> void StreamJsonRpc.Reflection.ProxyInputs.Options.get -> StreamJsonRpc.JsonRpcProxyOptions? StreamJsonRpc.Reflection.ProxyInputs.Options.init -> void StreamJsonRpc.Reflection.ProxyInputs.ProxyInputs() -> void +StreamJsonRpc.RpcTargetMetadata +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.Add(System.Type! iface) -> void +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.ClassAndInterfaces(System.Type! classType) -> void +StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate +StreamJsonRpc.RpcTargetMetadata.EventMetadata +StreamJsonRpc.RpcTargetMetadata.EventMetadata.CreateEventHandler.get -> StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.CreateEventHandler.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Event.get -> System.Reflection.EventInfo! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Event.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventHandlerType.get -> System.Type! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventHandlerType.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Name.get -> string! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Name.init -> void +StreamJsonRpc.RpcTargetMetadata.Events.get -> System.Collections.Generic.IReadOnlyList! +StreamJsonRpc.RpcTargetMetadata.Events.init -> void +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.Add(System.Type! iface) -> void +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.InterfaceCollection(System.Type! primaryInterface) -> void +StreamJsonRpc.RpcTargetMetadata.Methods.get -> System.Collections.Generic.IReadOnlyDictionary!>! +StreamJsonRpc.RpcTargetMetadata.Methods.init -> void +StreamJsonRpc.RpcTargetMetadata.RpcTargetMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.get -> StreamJsonRpc.JsonRpcMethodAttribute? +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Method.get -> System.Reflection.MethodInfo! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Method.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.get -> string! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.TargetMethodMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.TargetType.get -> System.Type! +StreamJsonRpc.RpcTargetMetadata.TargetType.init -> void +virtual StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate.Invoke(StreamJsonRpc.JsonRpc! rpc, string! eventName) -> System.Delegate! StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy.ProgressProxy(StreamJsonRpc.JsonRpc! rpc, object! token, bool useNamedArguments) -> void StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy.Report(T value) -> void diff --git a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt index eacf88869..432de8d70 100644 --- a/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/StreamJsonRpc/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,13 +1,24 @@ +override StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.ToString() -> string! static StreamJsonRpc.JsonRpcProxyOptions.Default.get -> StreamJsonRpc.JsonRpcProxyOptions! static StreamJsonRpc.NamedArgs.Create(System.Type! objectType, object? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.NamedArgs.Create(T? namedArgsObject) -> StreamJsonRpc.NamedArgs? static StreamJsonRpc.Reflection.ProxyBase.CreateProxy(StreamJsonRpc.JsonRpc! jsonRpc, in StreamJsonRpc.Reflection.ProxyInputs proxyInputs, bool startOrFail) -> StreamJsonRpc.IJsonRpcClientProxy! static StreamJsonRpc.Reflection.ProxyBase.TryCreateProxy(StreamJsonRpc.JsonRpc! jsonRpc, in StreamJsonRpc.Reflection.ProxyInputs proxyInputs, out StreamJsonRpc.IJsonRpcClientProxy? proxy) -> bool +static StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.Create(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! +static StreamJsonRpc.RpcTargetMetadata.EnableDynamicEventHandlerCreation() -> void +static StreamJsonRpc.RpcTargetMetadata.FromClass(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClass(System.Type! classType, StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! metadata) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClassNonPublic(System.Type! classType) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromClassNonPublic(System.Type! classType, StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces! metadata) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromInterface(StreamJsonRpc.RpcTargetMetadata.InterfaceCollection! interfaces) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.FromInterface(System.Type! rpcContract) -> StreamJsonRpc.RpcTargetMetadata! +static StreamJsonRpc.RpcTargetMetadata.RegisterEventArgs() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute StreamJsonRpc.ExportRpcContractProxiesAttribute.ExportRpcContractProxiesAttribute() -> void StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.get -> bool StreamJsonRpc.ExportRpcContractProxiesAttribute.ForbidExternalProxyGeneration.set -> void StreamJsonRpc.IJsonRpcClientProxy.As() -> T? +StreamJsonRpc.JsonRpc.AddLocalRpcTarget(StreamJsonRpc.RpcTargetMetadata! exposingMembersOn, object! target, StreamJsonRpc.JsonRpcTargetOptions? options) -> void override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.GetJsonSchema(Nerdbank.MessagePack.JsonSchemaContext! context, PolyType.Abstractions.ITypeShape! typeShape) -> System.Text.Json.Nodes.JsonObject? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Read(ref Nerdbank.MessagePack.MessagePackReader reader, Nerdbank.MessagePack.SerializationContext context) -> System.Collections.Generic.IAsyncEnumerable? override StreamJsonRpc.NerdbankMessagePackFormatter.AsyncEnumerableConverter.Write(ref Nerdbank.MessagePack.MessagePackWriter writer, in System.Collections.Generic.IAsyncEnumerable? value, Nerdbank.MessagePack.SerializationContext context) -> void @@ -80,6 +91,41 @@ StreamJsonRpc.Reflection.ProxyInputs.ContractInterface.init -> void StreamJsonRpc.Reflection.ProxyInputs.Options.get -> StreamJsonRpc.JsonRpcProxyOptions? StreamJsonRpc.Reflection.ProxyInputs.Options.init -> void StreamJsonRpc.Reflection.ProxyInputs.ProxyInputs() -> void +StreamJsonRpc.RpcTargetMetadata +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.Add(System.Type! iface) -> void +StreamJsonRpc.RpcTargetMetadata.ClassAndInterfaces.ClassAndInterfaces(System.Type! classType) -> void +StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate +StreamJsonRpc.RpcTargetMetadata.EventMetadata +StreamJsonRpc.RpcTargetMetadata.EventMetadata.CreateEventHandler.get -> StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.CreateEventHandler.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Event.get -> System.Reflection.EventInfo! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Event.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventHandlerType.get -> System.Type! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventHandlerType.init -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.EventMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Name.get -> string! +StreamJsonRpc.RpcTargetMetadata.EventMetadata.Name.init -> void +StreamJsonRpc.RpcTargetMetadata.Events.get -> System.Collections.Generic.IReadOnlyList! +StreamJsonRpc.RpcTargetMetadata.Events.init -> void +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.Add(System.Type! iface) -> void +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! +StreamJsonRpc.RpcTargetMetadata.InterfaceCollection.InterfaceCollection(System.Type! primaryInterface) -> void +StreamJsonRpc.RpcTargetMetadata.Methods.get -> System.Collections.Generic.IReadOnlyDictionary!>! +StreamJsonRpc.RpcTargetMetadata.Methods.init -> void +StreamJsonRpc.RpcTargetMetadata.RpcTargetMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.get -> StreamJsonRpc.JsonRpcMethodAttribute? +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Attribute.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Method.get -> System.Reflection.MethodInfo! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Method.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.get -> string! +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.Name.init -> void +StreamJsonRpc.RpcTargetMetadata.TargetMethodMetadata.TargetMethodMetadata() -> void +StreamJsonRpc.RpcTargetMetadata.TargetType.get -> System.Type! +StreamJsonRpc.RpcTargetMetadata.TargetType.init -> void +virtual StreamJsonRpc.RpcTargetMetadata.CreateEventHandlerDelegate.Invoke(StreamJsonRpc.JsonRpc! rpc, string! eventName) -> System.Delegate! StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy.ProgressProxy(StreamJsonRpc.JsonRpc! rpc, object! token, bool useNamedArguments) -> void StreamJsonRpc.Reflection.MessageFormatterProgressTracker.ProgressProxy.Report(T value) -> void diff --git a/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs b/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs index ad9cb972f..9863f4b1a 100644 --- a/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs +++ b/test/NativeAOTCompatibility.Test/NerdbankMessagePack.cs @@ -14,7 +14,11 @@ internal static async Task RunAsync() (Stream clientPipe, Stream serverPipe) = FullDuplexStream.CreatePair(); JsonRpc serverRpc = new JsonRpc(new LengthHeaderMessageHandler(serverPipe, serverPipe, CreateFormatter())); JsonRpc clientRpc = new JsonRpc(new LengthHeaderMessageHandler(clientPipe, clientPipe, CreateFormatter())); - serverRpc.AddLocalRpcMethod(nameof(Server.AddAsync), new Server().AddAsync); + + RpcTargetMetadata.RegisterEventArgs(); + var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer))); + serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null); + serverRpc.StartListening(); IServer proxy = clientRpc.Attach(); clientRpc.StartListening(); diff --git a/test/NativeAOTCompatibility.Test/Program.cs b/test/NativeAOTCompatibility.Test/Program.cs index 3cae00cd9..e63ccba6b 100644 --- a/test/NativeAOTCompatibility.Test/Program.cs +++ b/test/NativeAOTCompatibility.Test/Program.cs @@ -17,10 +17,21 @@ [JsonRpcContract] internal partial interface IServer { + event EventHandler Added; + Task AddAsync(int a, int b); } internal class Server : IServer { - public Task AddAsync(int a, int b) => Task.FromResult(a + b); + public event EventHandler? Added; + + public Task AddAsync(int a, int b) + { + int sum = a + b; + this.OnAdded(sum); + return Task.FromResult(sum); + } + + protected virtual void OnAdded(int sum) => this.Added?.Invoke(this, sum); } diff --git a/test/NativeAOTCompatibility.Test/SystemTextJson.cs b/test/NativeAOTCompatibility.Test/SystemTextJson.cs index e73c1a26b..c62ea04ec 100644 --- a/test/NativeAOTCompatibility.Test/SystemTextJson.cs +++ b/test/NativeAOTCompatibility.Test/SystemTextJson.cs @@ -12,7 +12,11 @@ internal static async Task RunAsync() (Stream clientPipe, Stream serverPipe) = FullDuplexStream.CreatePair(); JsonRpc serverRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter())); JsonRpc clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter())); - serverRpc.AddLocalRpcMethod(nameof(Server.AddAsync), new Server().AddAsync); + + RpcTargetMetadata.RegisterEventArgs(); + var targetMetadata = RpcTargetMetadata.FromInterface(new RpcTargetMetadata.InterfaceCollection(typeof(IServer))); + serverRpc.AddLocalRpcTarget(targetMetadata, new Server(), null); + serverRpc.StartListening(); IServer proxy = clientRpc.Attach(); clientRpc.StartListening(); diff --git a/test/StreamJsonRpc.Tests/JsonRpcMethodAttributeTests.cs b/test/StreamJsonRpc.Tests/JsonRpcMethodAttributeTests.cs index 3322eca22..69b76ee87 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcMethodAttributeTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcMethodAttributeTests.cs @@ -132,133 +132,147 @@ public async Task CallingClrMethodsThatHaveAttributeDefinedDoesNotAttemptToMatch } [Fact] - public void JsonRpcMethodAttribute_ConflictOverloadMethodsThrowsException() + public async Task JsonRpcMethodAttribute_ConflictOverloadMethodsThrowsException() { - var invalidServer = new ConflictingOverloadServer(); + var serverObject = new ConflictingOverloadServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); - } + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); - [Fact] - public void JsonRpcMethodAttribute_MissingAttributeOnOverloadMethodBeforeThrowsException() - { - var invalidServer = new MissingMethodAttributeOverloadBeforeServer(); + string result = await clientRpc.InvokeWithCancellationAsync("test/string", ["Andrew"], this.TimeoutToken).WithCancellation(this.TimeoutToken); + Assert.Equal("conflicting string: Andrew", result); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; - - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + result = await clientRpc.InvokeWithCancellationAsync("test/int", ["Andrew", 5], this.TimeoutToken).WithCancellation(this.TimeoutToken); + Assert.Equal("conflicting string: Andrew int: 5", result); } [Fact] - public void JsonRpcMethodAttribute_MissingAttributeOnOverloadMethodAfterThrowsException() + public async Task JsonRpcMethodAttribute_MissingAttributeOnOverloadMethodBefore() { - var invalidServer = new MissingMethodAttributeOverloadAfterServer(); + var serverObject = new MissingMethodAttributeOverloadBeforeServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); + + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + + string actual = await clientRpc.InvokeWithCancellationAsync(nameof(MissingMethodAttributeOverloadBeforeServer.InvokeOverloadConflictingMethodAttribute), ["hi"], this.TimeoutToken); + Assert.Equal("conflicting string: hi", actual); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + string actual2 = await clientRpc.InvokeWithCancellationAsync("test/string", ["hi", 5], this.TimeoutToken); + Assert.Equal("conflicting string: hi int: 5", actual2); } [Fact] - public void JsonRpcMethodAttribute_SameAttributeUsedOnDifferentDerivedMethodsThrowsException() + public async Task JsonRpcMethodAttribute_MissingAttributeOnOverloadMethodAfter() { - var invalidServer = new SameAttributeUsedOnDifferentDerivedMethodsServer(); + var serverObject = new MissingMethodAttributeOverloadAfterServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); + + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + string actual = await clientRpc.InvokeWithCancellationAsync("test/string", ["hi"], this.TimeoutToken); + Assert.Equal("conflicting string: hi", actual); + + string actual2 = await clientRpc.InvokeWithCancellationAsync(nameof(MissingMethodAttributeOverloadAfterServer.InvokeOverloadConflictingMethodAttribute), ["hi", 5], this.TimeoutToken); + Assert.Equal("conflicting string: hi int: 5", actual2); } [Fact] - public void JsonRpcMethodAttribute_SameAttributeUsedOnDifferentMethodsThrowsException() + public async Task JsonRpcMethodAttribute_SameAttributeUsedOnDifferentDerivedMethodsThrowsException() { - var invalidServer = new SameAttributeUsedOnDifferentMethodsServer(); + var serverObject = new SameAttributeUsedOnDifferentDerivedMethodsServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + + string actual = await clientRpc.InvokeWithCancellationAsync("base/InvokeMethodWithAttribute", ["hi"], this.TimeoutToken); + Assert.Equal("conflicting string: hi", actual); } [Fact] - public void JsonRpcMethodAttribute_ConflictOverrideMethodsThrowsException() + public async Task JsonRpcMethodAttribute_OverrideMethodChangesRpcName() { - var invalidServer = new InvalidOverrideServer(); + var serverObject = new OverrideAndRenameMethodServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + + string actual = await clientRpc.InvokeWithCancellationAsync("child/InvokeVirtualMethodOverride", [], this.TimeoutToken); + Assert.Equal("child InvokeVirtualMethodOverride", actual); + + await Assert.ThrowsAsync(() => clientRpc.InvokeWithCancellationAsync("base/InvokeVirtualMethodOverride", [], this.TimeoutToken)); } [Fact] - public void JsonRpcMethodAttribute_ReplacementNameIsAnotherBaseMethodNameServerThrowsException() + public async Task JsonRpcMethodAttribute_ReplacementNameIsAnotherBaseMethodNameServer() { - var invalidServer = new ReplacementNameIsAnotherBaseMethodNameServer(); + var serverObject = new ReplacementNameIsAnotherBaseMethodNameServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + + string actual = await clientRpc.InvokeWithCancellationAsync("Second", ["hi"], this.TimeoutToken); + Assert.Equal("third", actual); } [Fact] - public void JsonRpcMethodAttribute_ReplacementNameIsAnotherMethodNameThrowsException() + public async Task JsonRpcMethodAttribute_ReplacementNameIsAnotherMethodName() { - var invalidServer = new ReplacementNameIsAnotherMethodNameServer(); + var serverObject = new ReplacementNameIsAnotherMethodNameServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); + + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + + string actual = await clientRpc.InvokeWithCancellationAsync("Second", ["hi"], this.TimeoutToken); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + // The particular method that is invoked isn't well defined, but this one is the one + // that is invoked today, probably because it's "first" in declaration order. + Assert.Equal("first hi", actual); } [Fact] - public void JsonRpcMethodAttribute_InvalidAsyncMethodWithAsyncAddedInAttributeThrowsException() + public async Task JsonRpcMethodAttribute_AsyncMethodWithAsyncAddedInAttribute() { - var invalidServer = new InvalidAsyncMethodWithAsyncAddedInAttributeServer(); + var serverObject = new CollidingAsyncMethodWithAsyncAddedInAttributeServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); + + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + string result = await clientRpc.InvokeWithCancellationAsync("FirstAsync", ["Andrew"], this.TimeoutToken).WithCancellation(this.TimeoutToken); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + // The particular method that is invoked isn't well defined, but this one is the one + // that is invoked today, probably because it's "first" in declaration order. + Assert.Equal("firstAsync Andrew", result); } [Fact] - public void JsonRpcMethodAttribute_InvalidAsyncMethodWithAsyncRemovedInAttributeThrowsException() + public async Task JsonRpcMethodAttribute_AsyncMethodWithAsyncRemovedInAttribute() { - var invalidServer = new InvalidAsyncMethodWithAsyncRemovedInAttributeServer(); + var serverObject = new AsyncMethodWithAsyncRemovedInAttributeServer(); - var streams = Nerdbank.FullDuplexStream.CreateStreams(); - var serverStream = streams.Item1; - var clientStream = streams.Item2; + (Stream clientStream, Stream serverStream) = Nerdbank.FullDuplexStream.CreateStreams(); - Assert.Throws(() => JsonRpc.Attach(serverStream, clientStream, invalidServer)); - Assert.Throws(() => new JsonRpc(serverStream, clientStream, invalidServer)); + JsonRpc serverRpc = JsonRpc.Attach(serverStream, serverObject); + JsonRpc clientRpc = JsonRpc.Attach(clientStream); + string result = await clientRpc.InvokeWithCancellationAsync("First", ["Andrew"], this.TimeoutToken).WithCancellation(this.TimeoutToken); + + // The particular method that is invoked isn't well defined, but this one is the one + // that is invoked today, probably because it's "first" in declaration order. + Assert.Equal("firstAsync Andrew", result); } [Fact] @@ -333,16 +347,16 @@ public async virtual Task InvokeVirtualMethodAsync(string arg) } /// - /// This class is invalid because a derived method has a different value. + /// This class has a derived method with a different value from its base. /// - public class InvalidOverrideServer : BaseClass + public class OverrideAndRenameMethodServer : BaseClass { [JsonRpcMethod("child/InvokeVirtualMethodOverride")] public override string InvokeVirtualMethodOverride() => $"child {nameof(this.InvokeVirtualMethodOverride)}"; } /// - /// This class is invalid because overloaded methods have different values. + /// This class has overloaded methods with different values. /// public class ConflictingOverloadServer : BaseClass { @@ -354,7 +368,7 @@ public class ConflictingOverloadServer : BaseClass } /// - /// This class is invalid because an overloaded method is missing value. + /// This class has an overloaded method that's missing a . /// The method missing the attribute comes before the method with the attribute. /// public class MissingMethodAttributeOverloadBeforeServer : BaseClass @@ -366,7 +380,8 @@ public class MissingMethodAttributeOverloadBeforeServer : BaseClass } /// - /// This class is invalid because an overloaded method is missing value. + /// This class has method overloads where one has that renames it, + /// and another overload has no attribute. /// The method missing the attribute comes after the method with the attribute. /// public class MissingMethodAttributeOverloadAfterServer : BaseClass @@ -378,19 +393,7 @@ public class MissingMethodAttributeOverloadAfterServer : BaseClass } /// - /// This class is invalid because two different methods in the same class have the same value. - /// - public class SameAttributeUsedOnDifferentMethodsServer : BaseClass - { - [JsonRpcMethod("test/string")] - public string First(string test) => $"conflicting string: {test}"; - - [JsonRpcMethod("test/string")] - public string Second(string arg1, int arg2) => $"conflicting string: {arg1} int: {arg2}"; - } - - /// - /// This class is invalid because two different methods in the base and derived classes have the same value. + /// This class has two different methods in the base and derived classes that have the same value. /// public class SameAttributeUsedOnDifferentDerivedMethodsServer : BaseClass { @@ -417,18 +420,18 @@ public class ReplacementNameIsAnotherBaseMethodNameServer : Base public class ReplacementNameIsAnotherMethodNameServer { [JsonRpcMethod("Second")] - public string First(string test) => "first"; + public string First(string test) => $"first {test}"; - public string Second(string test) => "second"; + public string Second(string test) => $"second {test}"; } - public class InvalidAsyncMethodWithAsyncRemovedInAttributeServer + public class AsyncMethodWithAsyncRemovedInAttributeServer { [JsonRpcMethod("First")] public async virtual Task FirstAsync(string arg) { await Task.Yield(); - return $"first {arg}"; + return $"firstAsync {arg}"; } public async virtual Task First(string arg) @@ -438,12 +441,12 @@ public async virtual Task First(string arg) } } - public class InvalidAsyncMethodWithAsyncAddedInAttributeServer + public class CollidingAsyncMethodWithAsyncAddedInAttributeServer { public async virtual Task FirstAsync(string arg) { await Task.Yield(); - return $"first {arg}"; + return $"firstAsync {arg}"; } [JsonRpcMethod("FirstAsync")] diff --git a/test/StreamJsonRpc.Tests/JsonRpcTests.cs b/test/StreamJsonRpc.Tests/JsonRpcTests.cs index c72cabc55..432654436 100644 --- a/test/StreamJsonRpc.Tests/JsonRpcTests.cs +++ b/test/StreamJsonRpc.Tests/JsonRpcTests.cs @@ -1507,7 +1507,13 @@ public async Task SerializationFailureInResult_ThrowsToClient() public async Task AddLocalRpcTarget_UseSingleObjectParameterDeserialization() { var streams = FullDuplexStream.CreatePair(); - var rpc = new JsonRpc(streams.Item1, streams.Item2); + var rpc = new JsonRpc(streams.Item1, streams.Item2) + { + TraceSource = new TraceSource("Loopback", SourceLevels.Verbose) + { + Listeners = { new XunitTraceListener(this.Logger) }, + }, + }; rpc.AddLocalRpcTarget(new Server(), new JsonRpcTargetOptions { UseSingleObjectParameterDeserialization = true }); rpc.StartListening(); diff --git a/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs b/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs new file mode 100644 index 000000000..947cf7af8 --- /dev/null +++ b/test/StreamJsonRpc.Tests/RpcTargetMetadataTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +public class RpcTargetMetadataTests +{ + internal interface IRpcContractBase + { + event EventHandler BaseEvent; + + Task MethodBaseAsync(int value); + + [JsonRpcMethod("RenamedBaseMethod2")] + Task RenamedBaseMethod(); + + [JsonRpcIgnore] + Task IgnoredMethod(); + } + + internal interface IRpcContractDerived : IRpcContractBase + { + event EventHandler DerivedEvent; + + Task MethodDerivedAsync(int value); + } + + [Fact] + public void FromInterface_ReturnsInheritedMembers() + { + Type rpcContract = typeof(IRpcContractDerived); + + RpcTargetMetadata metadata = RpcTargetMetadata.FromInterface(rpcContract); + + Assert.Contains(metadata.Methods, m => m.Key == nameof(IRpcContractBase.MethodBaseAsync)); + Assert.Contains(metadata.Events, e => e.Event.Name == nameof(IRpcContractBase.BaseEvent)); + } + + [Fact] + public void FromInterface_ReturnsDirectMembers() + { + Type rpcContract = typeof(IRpcContractDerived); + + RpcTargetMetadata metadata = RpcTargetMetadata.FromInterface(rpcContract); + + Assert.Contains(metadata.Methods, m => m.Key == nameof(IRpcContractDerived.MethodDerivedAsync)); + Assert.Contains(metadata.Events, e => e.Event.Name == nameof(IRpcContractDerived.DerivedEvent)); + } + + [Fact] + public void MethodRenameInheritedFromInterface() + { + RpcTargetMetadata metadata = RpcTargetMetadata.FromClass(typeof(RpcContractDerivedClass)); + Assert.Contains(metadata.Methods, m => m.Key == "RenamedBaseMethod2"); + Assert.DoesNotContain(metadata.Methods, m => m.Key == nameof(IRpcContractBase.RenamedBaseMethod)); + } + + [Fact] + public void MethodRenameDirectlyOnClass() + { + RpcTargetMetadata metadata = RpcTargetMetadata.FromClass(typeof(RpcContractDerivedClass)); + Assert.Contains(metadata.Methods, m => m.Key == "RenamedDerivedMethod"); + Assert.DoesNotContain(metadata.Methods, m => m.Key == nameof(IRpcContractDerived.MethodDerivedAsync)); + } + + [Fact] + public void IgnoredMethodByInterfaceAttribute() + { + RpcTargetMetadata metadata = RpcTargetMetadata.FromClass(typeof(RpcContractDerivedClass)); + Assert.DoesNotContain(metadata.Methods, m => m.Key == nameof(IRpcContractBase.IgnoredMethod)); + } + + internal class MyEventArgs : EventArgs; + + internal class RpcContractDerivedClass : IRpcContractDerived + { + public event EventHandler? BaseEvent; + + public event EventHandler? DerivedEvent; + + public Task IgnoredMethod() => throw new NotImplementedException(); + + public Task MethodBaseAsync(int value) => throw new NotImplementedException(); + + [JsonRpcMethod("RenamedDerivedMethod")] + public Task MethodDerivedAsync(int value) => throw new NotImplementedException(); + + public Task RenamedBaseMethod() => throw new NotImplementedException(); + + internal void OnBaseEvent() => this.BaseEvent?.Invoke(this, EventArgs.Empty); + + internal void OnDerivedEvent(MyEventArgs e) => this.DerivedEvent?.Invoke(this, e); + } +}