using System; using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Prise.Proxy { /// /// This is the PriseProxy static class that encapsulates most of the boilerplate static methods in order to interact with the remote object (Plugin) /// TODO /// - Generic Methods (Task Do(T stuff)) /// - Events (not sure if this is ever possible) /// public static class PriseProxy { public static object Invoke(object remoteObject, MethodInfo targetMethod, object[] args) => Invoke(remoteObject, targetMethod, args, new PassthroughParameterConverter(), new PassthroughResultConverter()); public static object Invoke(object remoteObject, MethodInfo targetMethod, object[] args, IParameterConverter parameterConverter, IResultConverter resultConverter) { try { var localType = targetMethod.ReturnType; var remoteMethod = FindMethodOnObject(targetMethod, remoteObject); if (remoteMethod == null) throw new PriseProxyException( $"Target method {targetMethod.Name} is not found on Proxy Type {remoteObject.GetType().Name}."); object result = null; if (remoteMethod.IsGenericMethod) { var generic = remoteMethod.MakeGenericMethod(targetMethod.GetGenericArguments()); result = generic.Invoke(remoteObject, SerializeParameters(parameterConverter, remoteMethod, args)); } else result = remoteMethod.Invoke(remoteObject, SerializeParameters(parameterConverter, remoteMethod, args)); var remoteType = remoteMethod.ReturnType; if (remoteType.BaseType == typeof(System.Threading.Tasks.Task)) return resultConverter.ConvertToLocalTypeAsync(localType, remoteType, result as System.Threading.Tasks.Task); if (remoteType == typeof(System.Threading.Tasks.Task)) return resultConverter.ConvertToLocalTypeAsync(localType, remoteType, result as System.Threading.Tasks.Task); if (remoteType == typeof(void)) return null; return resultConverter.ConvertToLocalType(localType, remoteType, result); } catch (Exception ex) when (ex is TargetInvocationException) { throw ex.InnerException ?? ex; } } public static MethodInfo FindMethodOnObject(MethodInfo callingMethod, object targetObject) => FindMethodOnObject( targetObject, new Method(callingMethod.Name, callingMethod.ReturnType), callingMethod.GetParameters().Select(p => new Parameter(p.Name, p.ParameterType)).ToArray(), throwOnError: true)!; // Throws error when null public static MethodInfo? TryFindMethodOnObject(MethodInfo callingMethod, object targetObject) => FindMethodOnObject( targetObject, new Method(callingMethod.Name, callingMethod.ReturnType), callingMethod.GetParameters().Select(p => new Parameter(p.Name, p.ParameterType)).ToArray()); public static MethodInfo? FindMethodOnObject( object targetObject, Method method, Parameter[] parameters, MethodFindingStrategy strategy = MethodFindingStrategy.MethodNameMustMatch | MethodFindingStrategy.MethodReturnTypeMustMatch | MethodFindingStrategy.ParameterCountMustMatch | MethodFindingStrategy.ParameterTypeMustMatch, bool throwOnError = false) { bool isNameCorrect(MethodInfo targetMethod) => targetMethod.Name == method.Name; // First, find by method name var targetMethods = targetObject.GetType().GetMethods().AsEnumerable(); if (strategy.HasFlag(MethodFindingStrategy.MethodNameMustMatch)) targetMethods = targetMethods.Where(isNameCorrect); if (!targetMethods.Any()) if (throwOnError) throw new PriseProxyException( $"Target method {method.Name} is not found on Proxy Type {targetObject.GetType().Name}."); else return null; if (targetMethods.Count() == 1) return targetMethods.First(); bool isReturnTypeCorrect(MethodInfo targetMethod) => targetMethod.ReturnType == method.ReturnType; if (strategy.HasFlag(MethodFindingStrategy.MethodReturnTypeMustMatch)) // Second, find by method name AND return type targetMethods = targetMethods.Where(isReturnTypeCorrect); if (targetMethods.Count() == 1) return targetMethods.First(); bool isParameterCountCorrect(MethodInfo targetMethod) => targetMethod.GetParameters().Count() == parameters.Length; bool doAllParametersMatch(MethodInfo targetMethod) { var callingMethodParameters = parameters; var targetMethodParameters = targetMethod.GetParameters(); for (var index = 0; index < callingMethodParameters.Count(); index++) { var callingParam = callingMethodParameters[index]; if (callingParam.Type is null) throw new PriseProxyException( $"When using {nameof(MethodFindingStrategy.ParameterTypeMustMatch)}, Parameter.Type must be provided"); var targetParam = targetMethodParameters[index]; if (!(targetParam.Name == callingParam.Name && targetParam.ParameterType.Name == callingParam.Type.Name)) return false; } return true; } targetMethods = targetMethods.Where(targetMethod => (!strategy.HasFlag(MethodFindingStrategy.ParameterCountMustMatch) || isParameterCountCorrect(targetMethod)) && (!strategy.HasFlag(MethodFindingStrategy.ParameterTypeMustMatch) || doAllParametersMatch(targetMethod)) ); if (!targetMethods.Any()) if (throwOnError) throw new PriseProxyException( $"Target method {method.Name} is not found on Proxy Type {targetObject.GetType().Name}."); else return null; if (targetMethods.Count() > 1) if (throwOnError) throw new PriseProxyException( $"Target method {method.Name} could not be determined on object {targetObject.GetType().Name}."); else return null; return targetMethods.First(); } internal static object[] SerializeParameters(IParameterConverter parameterConverter, MethodInfo targetMethod, object[] args) { var parameters = targetMethod.GetParameters(); var results = new List(); for (var index = 0; index < parameters.Count(); index++) { var parameter = parameters[index]; var parameterValue = args[index]; if (parameter.ParameterType.BaseType == typeof(System.MulticastDelegate)) { if (parameter.ParameterType.GenericTypeArguments.Any(g => g != typeof(EventArgs))) throw new PriseProxyException($"Custom EventArgs are not supported in Prise"); results.Add(parameterValue); continue; } object result = null; if (parameter.ParameterType.IsGenericParameter) { var runtimeType = parameterValue.GetType(); result = parameterConverter.ConvertToRemoteType(runtimeType, parameterValue); } else result = parameterConverter.ConvertToRemoteType(parameter.ParameterType, parameterValue); results.Add(result); } return results.ToArray(); } } /// /// This is the PriseProxy wrapper class that will acts as the communication layer between the Host and the Plugin. /// Every call from the Host to the Plugin will go through here. /// /// The Plugin type public class PriseProxy : DispatchProxy, IDisposable { private IParameterConverter parameterConverter; private IResultConverter resultConverter; private object remoteObject; protected bool disposed = false; protected override object Invoke(MethodInfo targetMethod, object[] args) => PriseProxy.Invoke(this.remoteObject, targetMethod, args, this.parameterConverter, this.resultConverter); public static object Create() => Create>(); internal PriseProxy SetRemoteObject(object remoteObject) { if (remoteObject == null) throw new PriseProxyException($"Remote object for Proxy<{typeof(T).Name}> was null"); this.remoteObject = remoteObject; return this; } internal PriseProxy SetParameterConverter(IParameterConverter parameterConverter) { if (parameterConverter == null) throw new PriseProxyException($"IParameterConverter for Proxy<{typeof(T).Name}> was null"); this.parameterConverter = parameterConverter; return this; } internal PriseProxy SetResultConverter(IResultConverter resultConverter) { if (resultConverter == null) throw new PriseProxyException($"IResultConverter for Proxy<{typeof(T).Name}> was null"); this.resultConverter = resultConverter; return this; } protected virtual void Dispose(bool disposing) { if (!this.disposed && disposing) { this.parameterConverter = null; this.resultConverter = null; this.remoteObject = null; } this.disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } }