My Implementation is Better

Code on this blog can rarely be considered remotely production-worthy. Today's example could possibly take the crown as the most filthy.

First, some context. I recently was working on a project where I wanted to represent ids as custom types instead of having ints everywhere. I found a package - StonglyTypedId - which is a source generator to remove most of the boilerplate for these strongly typed ids. Andrew Lock, the package author, has a series of blog posts describing this problem he calls primitive obsession as well as how this package was designed to solve it.

In using this StronglyTypedId library with Dapper.Contrib and MySQL I came across an issue where the .NET method Convert.ChangeType is used to convert from a ulong into the generated custom PersonId type. Convert.ChangeType can not be extended for custom types and hence the bug. The solution would be to either change Dapper.Contrib to use Dappers SqlMapper type conversion system or to use the .NET TypeConverter (example below) or some other extensible conversion method.

var converter = TypeDescriptor.GetConverter(typeof(idp.PropertyType));
var newValue = converter.ConvertFrom(id);
idp.SetValue(entityToInsert, newValue, null);

However practical a solution like that may be, it wouldn't make for a good blog post. What if, being stubborn individuals, we really wanted to extend Convert.ChangeType? Is that possible? Where would such a persuit take us? The implementation is pretty limited and so I say replace it - my implementation is better!

Here's an example of what I would want to run any time Convert.ChangeType is called.

private static object MyChangeType(object value, Type type)
{
    if (value is ulong ul && type == typeof(PersonId))
    {
        return new PersonId((int)ul);
    }

    return ChangeType(value, type);
}

As impossible as it seems, there is a way to do this. The trick is that Dapper.Contrib uses dynamic for the first parameter of Convert.ChangeType which means that the JIT does not know which method overload of Convert.ChangeType will be invoked until the moment when it is invoked. This gives us an opportunity to hack away at the Convert.ChangeType runtime method information in advance of the JIT knowing which method to call.

public static void Swap(MethodInfo target, MethodInfo replacement, MethodInfo backup)
{
    var runtimeMethodInfoType = Type.GetType("System.Reflection.RuntimeMethodInfo");
    var handleField = runtimeMethodInfoType.GetField("m_handle", BindingFlags.Instance | BindingFlags.NonPublic);
    handleField.SetValue(backup, handleField.GetValue(target));
    handleField.SetValue(target, handleField.GetValue(replacement));
}

The above code essentially swiches the pointers around for a few methods. It stores the original pointer in a backup method and replaces the target with the pointer from the replacement. This allow us to switch out the implementation of Convert.ChangeType as follows

public static void Main()
{
    SwapConvertChangeType();

    var id = Convert.ChangeType((dynamic)(ulong)123, typeof(PersonId));
    
    Console.WriteLine($"{id.GetType()} {id}");
}

private static void SwapConvertChangeType()
{
    var original = typeof(Convert)
        .GetMethods(BindingFlags.Public | BindingFlags.Static)
        .Single(m => m.Name == nameof(Convert.ChangeType) && m.GetParameters().Length == 2 && m.GetParameters()[1].ParameterType == typeof(Type));
    var backup = typeof(Program).GetMethod(nameof(OriginalChangeType), BindingFlags.Static | BindingFlags.NonPublic);
    var replacement = typeof(Program).GetMethod(nameof(ReplacementChangeType), BindingFlags.Static | BindingFlags.NonPublic);
    
    MethodSwapper.Swap(original, replacement, backup);
}

private static object ReplacementChangeType(object value, Type type)
{
    if (value is ulong ul && type == typeof(PersonId))
    {
        return new PersonId((int)ul);
    }

    return OriginalChangeType(value, type);
}

private static object OriginalChangeType(object value, Type type) => throw new NotImplementedException();
    

When we execute the program, we get StronglyTypedIdTest.PersonId 123. The fix also works when running this within the context of Dapper.Contrib. I plan to study the runtime in more detail to provide a more detailed explanation in a future blog post.

Given how hacky the solution is, I ended up using ints instead.

Merry Christmas


Other posts you might like


Join the Discussion

You must be signed in to comment.

No user information will be stored on our site until you comment.