From 309806e6deef70ee8e04e9f7274fb81807c4ddc7 Mon Sep 17 00:00:00 2001 From: Mark Jerzykowski Date: Thu, 2 Nov 2017 23:39:31 +0000 Subject: [PATCH] Support generic methods in methodinfoserializerfactory --- Hyperion.Tests/ExpressionTests.cs | 16 +++ Hyperion/Extensions/TypeEx.cs | 113 ++++++++++++++++++ .../MethodInfoSerializerFactory.cs | 37 +++--- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/Hyperion.Tests/ExpressionTests.cs b/Hyperion.Tests/ExpressionTests.cs index 26cf122b..c977f8f2 100644 --- a/Hyperion.Tests/ExpressionTests.cs +++ b/Hyperion.Tests/ExpressionTests.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.Serialization; @@ -474,6 +475,21 @@ public void CanSerializeLambdaExpression() } } + [Fact] + public void CanSerializeLambdaExpressionContainingGenericMethod() { + Expression> expr = dummy => dummy.TestField.Contains('s'); + var serializer = new Serializer(new SerializerOptions(preserveObjectReferences: true)); + using (var ms = new MemoryStream()) + { + serializer.Serialize(expr, ms); + ms.Seek(0, SeekOrigin.Begin); + var deserialized = serializer.Deserialize>>(ms); + Assert.NotNull(((MethodCallExpression)deserialized.Body).Method); + Assert.True(deserialized.Compile()(new Dummy("sausages"))); + Assert.False(deserialized.Compile()(new Dummy("field"))); + } + } + [Fact] public void CanSerializeInvocationExpression() { diff --git a/Hyperion/Extensions/TypeEx.cs b/Hyperion/Extensions/TypeEx.cs index d139d8d7..30199220 100644 --- a/Hyperion/Extensions/TypeEx.cs +++ b/Hyperion/Extensions/TypeEx.cs @@ -223,5 +223,118 @@ public static string ToQualifiedAssemblyName(string shortName) var res = shortName.Replace(",%core%", CoreAssemblyName); return res; } + + /// + /// Search for a method by name, parameter types, and binding flags. + /// Unlike GetMethod(), does 'loose' matching on generic + /// parameter types, and searches base interfaces. + /// + /// + public static MethodInfo GetMethodExt(this Type thisType, + string name, + BindingFlags bindingFlags, + params Type[] parameterTypes) + { + MethodInfo matchingMethod = null; + + // Check all methods with the specified name, including in base classes + GetMethodExt(ref matchingMethod, thisType, name, bindingFlags, parameterTypes); + + // If we're searching an interface, we have to manually search base interfaces + if (matchingMethod == null && thisType.GetTypeInfo().IsInterface) + { + foreach (Type interfaceType in thisType.GetInterfaces()) + GetMethodExt(ref matchingMethod, + interfaceType, + name, + bindingFlags, + parameterTypes); + } + + return matchingMethod; + } + + private static void GetMethodExt(ref MethodInfo matchingMethod, + Type type, + string name, + BindingFlags bindingFlags, + params Type[] parameterTypes) + { + // Check all methods with the specified name, including in base classes + foreach (MethodInfo methodInfo in type.GetTypeInfo().GetMember(name, + MemberTypes.Method, + bindingFlags)) + { + // Check that the parameter counts and types match, + // with 'loose' matching on generic parameters + ParameterInfo[] parameterInfos = methodInfo.GetParameters(); + if (parameterInfos.Length == parameterTypes.Length) + { + int i = 0; + for (; i < parameterInfos.Length; ++i) + { + if (!parameterInfos[i].ParameterType + .IsSimilarType(parameterTypes[i])) + break; + } + if (i == parameterInfos.Length) + { + if (matchingMethod == null) + matchingMethod = methodInfo; + else + throw new AmbiguousMatchException( + "More than one matching method found!"); + } + } + } + } + + /// + /// Special type used to match any generic parameter type in GetMethodExt(). + /// + public class T + { } + + /// + /// Determines if the two types are either identical, or are both generic + /// parameters or generic types with generic parameters in the same + /// locations (generic parameters match any other generic paramter, + /// and concrete types). + /// + private static bool IsSimilarType(this Type thisType, Type type) + { + // Ignore any 'ref' types + if (thisType.IsByRef) + thisType = thisType.GetElementType(); + if (type.IsByRef) + type = type.GetElementType(); + + // Handle array types + if (thisType.IsArray && type.IsArray) + return thisType.GetElementType().IsSimilarType(type.GetElementType()); + + // If the types are identical, or they're both generic parameters + // or the special 'T' type, treat as a match + if (thisType == type || thisType.IsGenericParameter || thisType == typeof(T) || type.IsGenericParameter || type == typeof(T)) + return true; + + // Handle any generic arguments + if (thisType.GetTypeInfo().IsGenericType && type.GetTypeInfo().IsGenericType) + { + Type[] thisArguments = thisType.GetGenericArguments(); + Type[] arguments = type.GetGenericArguments(); + if (thisArguments.Length == arguments.Length) + { + for (int i = 0; i < thisArguments.Length; ++i) + { + if (!thisArguments[i].IsSimilarType(arguments[i])) + return false; + } + return true; + } + } + + return false; + } } } \ No newline at end of file diff --git a/Hyperion/SerializerFactories/MethodInfoSerializerFactory.cs b/Hyperion/SerializerFactories/MethodInfoSerializerFactory.cs index 36435819..2b95574a 100644 --- a/Hyperion/SerializerFactories/MethodInfoSerializerFactory.cs +++ b/Hyperion/SerializerFactories/MethodInfoSerializerFactory.cs @@ -37,37 +37,32 @@ public override ValueSerializer BuildSerializer(Serializer serializer, Type type { var name = stream.ReadString(session); var owner = stream.ReadObject(session) as Type; - var arguments = stream.ReadObject(session) as Type[]; + var parameterTypes = stream.ReadObject(session) as Type[]; + var method = owner.GetMethodExt(name, + BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + parameterTypes); + if (method.IsGenericMethodDefinition) { + var genericTypeArguments = stream.ReadObject(session) as Type[]; + method = method.MakeGenericMethod(genericTypeArguments); + } -#if NET45 - var method = owner.GetTypeInfo().GetMethod( - name, - BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, - CallingConventions.Any, - arguments, - null); return method; -#else - var methods = owner.GetTypeInfo() - .GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | - BindingFlags.NonPublic); - var method = methods.FirstOrDefault(m => m.Name == name && - m.GetParameters() - .Select(p => p.ParameterType) - .SequenceEqual(arguments)); - return method; -#endif }; ObjectWriter writer = (stream, obj, session) => { var method = (MethodInfo) obj; var name = method.Name; var owner = method.DeclaringType; - var arguments = method.GetParameters().Select(p => p.ParameterType).ToArray(); - StringSerializer.WriteValueImpl(stream,name,session); + StringSerializer.WriteValueImpl(stream, name, session); stream.WriteObjectWithManifest(owner, session); + var arguments = method.GetParameters().Select(p => p.ParameterType).ToArray(); stream.WriteObjectWithManifest(arguments, session); + if (method.IsGenericMethod) { + // we use the parameter types to find the method above but, if generic, we need to store the generic type arguments as well + // in order to MakeGenericType + var genericTypeArguments = method.GetGenericArguments(); + stream.WriteObjectWithManifest(genericTypeArguments, session); + } }; os.Initialize(reader, writer);