Skip to content

Instantly share code, notes, and snippets.

@madelson
Last active August 24, 2021 11:32
Show Gist options
  • Save madelson/cbb9786d227c511619ae to your computer and use it in GitHub Desktop.
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
Copy link

This is great! Would you consider putting it in a nuget package?

@madelson
Copy link
Author

@howcheng
Copy link

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.

@madelson
Copy link
Author

madelson commented Aug 24, 2021

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment