-
-
Save madelson/cbb9786d227c511619ae to your computer and use it in GitHub Desktop.
| // ************** Implementation ************** | |
| namespace CodeDucky | |
| { | |
| using Microsoft.CSharp.RuntimeBinder; | |
| using System; | |
| using System.Collections; | |
| using System.Collections.Concurrent; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Linq.Expressions; | |
| using System.Reflection; | |
| using System.Runtime.CompilerServices; | |
| using System.Text; | |
| using System.Text.RegularExpressions; | |
| using System.Threading.Tasks; | |
| using CSharpBinder = Microsoft.CSharp.RuntimeBinder.Binder; | |
| public static class TypeHelpers | |
| { | |
| #region ---- Explicit casts ---- | |
| public static bool IsCastableTo(this Type from, Type to) | |
| { | |
| // from http://www.codeducky.org/10-utilities-c-developers-should-know-part-one/ | |
| Throw.IfNull(from, "from"); | |
| Throw.IfNull(to, "to"); | |
| // explicit conversion always works if to : from OR if | |
| // there's an implicit conversion | |
| if (from.IsAssignableFrom(to) || from.IsImplicitlyCastableTo(to)) | |
| { | |
| return true; | |
| } | |
| var key = new KeyValuePair<Type, Type>(from, to); | |
| bool cachedValue; | |
| if (CastCache.TryGetCachedValue(key, out cachedValue)) | |
| { | |
| return cachedValue; | |
| } | |
| // for nullable types, we can simply strip off the nullability and evaluate the underyling types | |
| var underlyingFrom = Nullable.GetUnderlyingType(from); | |
| var underlyingTo = Nullable.GetUnderlyingType(to); | |
| if (underlyingFrom != null || underlyingTo != null) | |
| { | |
| return (underlyingFrom ?? from).IsCastableTo(underlyingTo ?? to); | |
| } | |
| bool result; | |
| if (from.IsValueType) | |
| { | |
| try | |
| { | |
| ReflectionHelpers.GetMethod(() => AttemptExplicitCast<object, object>()) | |
| .GetGenericMethodDefinition() | |
| .MakeGenericMethod(from, to) | |
| .Invoke(null, new object[0]); | |
| result = true; | |
| } | |
| catch (TargetInvocationException ex) | |
| { | |
| result = !( | |
| ex.InnerException is RuntimeBinderException | |
| // if the code runs in an environment where this message is localized, we could attempt a known failure first and base the regex on it's message | |
| && Regex.IsMatch(ex.InnerException.Message, @"^Cannot convert type '.*' to '.*'$") | |
| ); | |
| } | |
| } | |
| else | |
| { | |
| // if the from type is null, the dynamic logic above won't be of any help because | |
| // either both types are nullable and thus a runtime cast of null => null will | |
| // succeed OR we get a runtime failure related to the inability to cast null to | |
| // the desired type, which may or may not indicate an actual issue. thus, we do | |
| // the work manually | |
| result = from.IsNonValueTypeExplicitlyCastableTo(to); | |
| } | |
| CastCache.UpdateCache(key, result); | |
| return result; | |
| } | |
| private static bool IsNonValueTypeExplicitlyCastableTo(this Type from, Type to) | |
| { | |
| if ((to.IsInterface && !from.IsSealed) | |
| || (from.IsInterface && !to.IsSealed)) | |
| { | |
| // any non-sealed type can be cast to any interface since the runtime type MIGHT implement | |
| // that interface. The reverse is also true; we can cast to any non-sealed type from any interface | |
| // since the runtime type that implements the interface might be a derived type of to. | |
| return true; | |
| } | |
| // arrays are complex because of array covariance | |
| // (see http://msmvps.com/blogs/jon_skeet/archive/2013/06/22/array-covariance-not-just-ugly-but-slow-too.aspx). | |
| // Thus, we have to allow for things like var x = (IEnumerable<string>)new object[0]; | |
| // and var x = (object[])default(IEnumerable<string>); | |
| var arrayType = from.IsArray && !from.GetElementType().IsValueType ? from | |
| : to.IsArray && !to.GetElementType().IsValueType ? to | |
| : null; | |
| if (arrayType != null) | |
| { | |
| var genericInterfaceType = from.IsInterface && from.IsGenericType ? from | |
| : to.IsInterface && to.IsGenericType ? to | |
| : null; | |
| if (genericInterfaceType != null) | |
| { | |
| return arrayType.GetInterfaces() | |
| .Any(i => i.IsGenericType | |
| && i.GetGenericTypeDefinition() == genericInterfaceType.GetGenericTypeDefinition() | |
| && i.GetGenericArguments().Zip(to.GetGenericArguments(), (ia, ta) => ta.IsAssignableFrom(ia) || ia.IsAssignableFrom(ta)).All(b => b)); | |
| } | |
| } | |
| // look for conversion operators. Even though we already checked for implicit conversions, we have to look | |
| // for operators of both types because, for example, if a class defines an implicit conversion to int then it can be explicitly | |
| // cast to uint | |
| const BindingFlags conversionFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; | |
| var conversionMethods = from.GetMethods(conversionFlags) | |
| .Concat(to.GetMethods(conversionFlags)) | |
| .Where(m => (m.Name == "op_Explicit" || m.Name == "op_Implicit") | |
| && m.Attributes.HasFlag(MethodAttributes.SpecialName) | |
| && m.GetParameters().Length == 1 | |
| && ( | |
| // the from argument of the conversion function can be an indirect match to from in | |
| // either direction. For example, if we have A : B and Foo defines a conversion from B => Foo, | |
| // then C# allows A to be cast to Foo | |
| m.GetParameters()[0].ParameterType.IsAssignableFrom(from) | |
| || from.IsAssignableFrom(m.GetParameters()[0].ParameterType) | |
| ) | |
| ); | |
| if (to.IsPrimitive && typeof(IConvertible).IsAssignableFrom(to)) | |
| { | |
| // as mentioned above, primitive convertible types (i. e. not IntPtr) get special | |
| // treatment in the sense that if you can convert from Foo => int, you can convert | |
| // from Foo => double as well | |
| return conversionMethods.Any(m => m.ReturnType.IsCastableTo(to)); | |
| } | |
| return conversionMethods.Any(m => m.ReturnType == to); | |
| } | |
| private static void AttemptExplicitCast<TFrom, TTo>() | |
| { | |
| // based on the IL generated from | |
| // var x = (TTo)(dynamic)default(TFrom); | |
| var binder = CSharpBinder.Convert(CSharpBinderFlags.ConvertExplicit, typeof(TTo), typeof(TypeHelpers)); | |
| var callSite = CallSite<Func<CallSite, TFrom, TTo>>.Create(binder); | |
| callSite.Target(callSite, default(TFrom)); | |
| } | |
| #endregion | |
| #region ---- Implicit casts ---- | |
| public static bool IsImplicitlyCastableTo(this Type from, Type to) | |
| { | |
| // from http://www.codeducky.org/10-utilities-c-developers-should-know-part-one/ | |
| Throw.IfNull(from, "from"); | |
| Throw.IfNull(to, "to"); | |
| // not strictly necessary, but speeds things up and avoids polluting the cache | |
| if (to.IsAssignableFrom(from)) | |
| { | |
| return true; | |
| } | |
| var key = new KeyValuePair<Type, Type>(from, to); | |
| bool cachedValue; | |
| if (ImplicitCastCache.TryGetCachedValue(key, out cachedValue)) | |
| { | |
| return cachedValue; | |
| } | |
| bool result; | |
| try | |
| { | |
| // overload of GetMethod() from http://www.codeducky.org/10-utilities-c-developers-should-know-part-two/ | |
| // that takes Expression<Action> | |
| ReflectionHelpers.GetMethod(() => AttemptImplicitCast<object, object>()) | |
| .GetGenericMethodDefinition() | |
| .MakeGenericMethod(from, to) | |
| .Invoke(null, new object[0]); | |
| result = true; | |
| } | |
| catch (TargetInvocationException ex) | |
| { | |
| result = !( | |
| ex.InnerException is RuntimeBinderException | |
| // if the code runs in an environment where this message is localized, we could attempt a known failure first and base the regex on it's message | |
| && Regex.IsMatch(ex.InnerException.Message, @"^The best overloaded method match for 'System.Collections.Generic.List<.*>.Add(.*)' has some invalid arguments$") | |
| ); | |
| } | |
| ImplicitCastCache.UpdateCache(key, result); | |
| return result; | |
| } | |
| private static void AttemptImplicitCast<TFrom, TTo>() | |
| { | |
| // based on the IL produced by: | |
| // dynamic list = new List<TTo>(); | |
| // list.Add(default(TFrom)); | |
| // We can't use the above code because it will mimic a cast in a generic method | |
| // which doesn't have the same semantics as a cast in a non-generic method | |
| var list = new List<TTo>(capacity: 1); | |
| var binder = CSharpBinder.InvokeMember( | |
| flags: CSharpBinderFlags.ResultDiscarded, | |
| name: "Add", | |
| typeArguments: null, | |
| context: typeof(TypeHelpers), | |
| argumentInfo: new[] | |
| { | |
| CSharpArgumentInfo.Create(flags: CSharpArgumentInfoFlags.None, name: null), | |
| CSharpArgumentInfo.Create( | |
| flags: CSharpArgumentInfoFlags.UseCompileTimeType, | |
| name: null | |
| ), | |
| } | |
| ); | |
| var callSite = CallSite<Action<CallSite, object, TFrom>>.Create(binder); | |
| callSite.Target.Invoke(callSite, list, default(TFrom)); | |
| } | |
| #endregion | |
| #region ---- Caching ---- | |
| private const int MaxCacheSize = 5000; | |
| private static readonly Dictionary<KeyValuePair<Type, Type>, bool> CastCache = new Dictionary<KeyValuePair<Type, Type>, bool>(), | |
| ImplicitCastCache = new Dictionary<KeyValuePair<Type, Type>, bool>(); | |
| private static bool TryGetCachedValue<TKey, TValue>(this Dictionary<TKey, TValue> cache, TKey key, out TValue value) | |
| { | |
| lock (cache.As<ICollection>().SyncRoot) | |
| { | |
| return cache.TryGetValue(key, out value); | |
| } | |
| } | |
| private static void UpdateCache<TKey, TValue>(this Dictionary<TKey, TValue> cache, TKey key, TValue value) | |
| { | |
| lock (cache.As<ICollection>().SyncRoot) | |
| { | |
| if (cache.Count > MaxCacheSize) | |
| { | |
| cache.Clear(); | |
| } | |
| cache[key] = value; | |
| } | |
| } | |
| #endregion | |
| } | |
| } | |
| // ************** Tests ************** | |
| namespace CodeDucky | |
| { | |
| using Microsoft.CSharp; | |
| using Microsoft.VisualStudio.TestTools.UnitTesting; | |
| using System; | |
| using System.CodeDom.Compiler; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Text; | |
| using System.Threading.Tasks; | |
| [TestClass] | |
| public class ConversionsTest | |
| { | |
| [TestMethod] | |
| public void ImplicitlyCastable() | |
| { | |
| this.RunTests((from, to) => from.IsImplicitlyCastableTo(to), @implicit: true); | |
| } | |
| [TestMethod] | |
| public void ExplicitlyCastable() | |
| { | |
| this.RunTests((from, to) => from.IsCastableTo(to), @implicit: false); | |
| } | |
| /// <summary> | |
| /// Validates the given implementation function for either implicit or explicit conversion | |
| /// </summary> | |
| private void RunTests(Func<Type, Type, bool> func, bool @implicit) | |
| { | |
| // gather types | |
| var primitives = typeof(object).Assembly.GetTypes().Where(t => t.IsPrimitive).ToArray(); | |
| var simpleTypes = new[] { typeof(string), typeof(DateTime), typeof(decimal), typeof(object), typeof(DateTimeOffset), typeof(TimeSpan), typeof(StringSplitOptions), typeof(DateTimeKind) }; | |
| var variantTypes = new[] { typeof(string[]), typeof(object[]), typeof(IEnumerable<string>), typeof(IEnumerable<object>), typeof(Func<string>), typeof(Func<object>), typeof(Action<string>), typeof(Action<object>) }; | |
| var conversionOperators = new[] { typeof(Operators), typeof(Operators2), typeof(DerivedOperators), typeof(OperatorsStruct) }; | |
| var typesToConsider = primitives.Concat(simpleTypes).Concat(variantTypes).Concat(conversionOperators).ToArray(); | |
| var allTypesToConsider = typesToConsider.Concat(typesToConsider.Where(t => t.IsValueType).Select(t => typeof(Nullable<>).MakeGenericType(t))); | |
| // generate test cases | |
| var cases = this.GenerateTestCases(allTypesToConsider, @implicit); | |
| // collect errors | |
| var mistakes = new List<string>(); | |
| foreach (var @case in cases) | |
| { | |
| var result = func(@case.Item1, @case.Item2); | |
| if (result != (@case.Item3 == null)) | |
| { | |
| // func(@case.Item1, @case.Item2); // break here for easy debugging | |
| mistakes.Add(string.Format("{0} => {1}: got {2} for {3} cast", @case.Item1, @case.Item2, result, @implicit ? "implicit" : "explicit")); | |
| } | |
| } | |
| Assert.IsTrue(mistakes.Count == 0, string.Join(Environment.NewLine, new[] { mistakes.Count + " errors" }.Concat(mistakes))); | |
| } | |
| private List<Tuple<Type, Type, CompilerError>> GenerateTestCases(IEnumerable<Type> types, bool @implicit) | |
| { | |
| // gather all pairs | |
| var typeCrossProduct = types.SelectMany(t => types, (from, to) => new { from, to }) | |
| .Select((t, index) => new { t.from, t.to, index }) | |
| .ToArray(); | |
| // create the code to pass to the compiler | |
| var code = string.Join( | |
| Environment.NewLine, | |
| new[] { "namespace A { public class B { static T Get<T>() { return default(T); } public void C() {" } | |
| .Concat(typeCrossProduct.Select(t => string.Format("{0} var{1} = {2}Get<{3}>();", GetName(t.to), t.index, @implicit ? string.Empty : "(" + GetName(t.to) + ")", GetName(t.from)))) | |
| .Concat(new[] { "}}}" }) | |
| ); | |
| // compile the code | |
| var provider = new CSharpCodeProvider(); | |
| var compilerParams = new CompilerParameters(); | |
| compilerParams.ReferencedAssemblies.Add(this.GetType().Assembly.Location); // reference the current assembly! | |
| compilerParams.GenerateExecutable = false; | |
| compilerParams.GenerateInMemory = true; | |
| var compilationResult = provider.CompileAssemblyFromSource(compilerParams, code); | |
| // determine the outcome of each conversion by matching compiler errors with conversions by line # | |
| var cases = typeCrossProduct.GroupJoin( | |
| compilationResult.Errors.Cast<CompilerError>(), | |
| t => t.index, | |
| e => e.Line - 2, | |
| (t, e) => Tuple.Create(t.from, t.to, e.FirstOrDefault()) | |
| ) | |
| .ToList(); | |
| // add a special case | |
| // this can't be verified by the normal means, since it's a private class | |
| cases.Add(Tuple.Create(typeof(PrivateOperators), typeof(int), default(CompilerError))); | |
| return cases; | |
| } | |
| /// <summary> | |
| /// Gets a C# name for the given type | |
| /// </summary> | |
| private static string GetName(Type type) | |
| { | |
| if (!type.IsGenericType) | |
| { | |
| return type.ToString(); | |
| } | |
| return string.Format("{0}.{1}<{2}>", type.Namespace, type.Name.Substring(0, type.Name.IndexOf('`')), string.Join(", ", type.GetGenericArguments().Select(GetName))); | |
| } | |
| private class PrivateOperators | |
| { | |
| public static implicit operator int(PrivateOperators o) | |
| { | |
| return 1; | |
| } | |
| } | |
| } | |
| public class Operators | |
| { | |
| public static implicit operator string(Operators o) | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| public static implicit operator int(Operators o) | |
| { | |
| return 1; | |
| } | |
| public static explicit operator decimal?(Operators o) | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| public static explicit operator StringSplitOptions(Operators o) | |
| { | |
| return StringSplitOptions.RemoveEmptyEntries; | |
| } | |
| } | |
| public class DerivedOperators : Operators | |
| { | |
| public static explicit operator DateTime(DerivedOperators o) | |
| { | |
| return DateTime.Now; | |
| } | |
| } | |
| public struct OperatorsStruct | |
| { | |
| public static implicit operator string(OperatorsStruct o) | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| public static implicit operator int(OperatorsStruct o) | |
| { | |
| return 1; | |
| } | |
| public static explicit operator decimal?(OperatorsStruct o) | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| public static explicit operator StringSplitOptions(OperatorsStruct o) | |
| { | |
| return StringSplitOptions.RemoveEmptyEntries; | |
| } | |
| } | |
| public class Operators2 | |
| { | |
| public static explicit operator bool(Operators2 o) | |
| { | |
| return false; | |
| } | |
| public static implicit operator Operators2(DerivedOperators o) | |
| { | |
| return null; | |
| } | |
| public static explicit operator Operators2(int i) | |
| { | |
| throw new NotImplementedException(); | |
| } | |
| } | |
| } |
@BrannJoly I'm actually in the progress of working on one. See https://github.com/madelson/MedallionUtilities/tree/master/MedallionReflection
Have you tried this in .NET Core? I was getting an exception in this block:
ReflectionHelper.GetMethod(() => AttemptImplicitCast<object, object>())
.GetGenericMethodDefinition()
.MakeGenericMethod(from, to) // this line
.Invoke(null, new object[0]);
"System.BadImageFormatException : An attempt was made to load a program with an incorrect format. (0x8007000B)". In the specific use case I had, from is a string (or more specifically, a ReadOnlySpan<char>) and to is an integer.
@howcheng Interesting. It's because ReadOnlySpan<char> is a ref struct type so you can't use it as a generic argument. Since dynamic doesn't work with ref structs, we'd need to fork the implementation upon detecting IsByRefType if we wanted it to work.
I imagine that in that case we're reduced to reflecting for op_implicit/op_explicit static methods on each type :-(
I've filed an issue to fix this in the library version here: madelson/MedallionUtilities#6
This is great! Would you consider putting it in a nuget package?