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 int
s 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 int
s instead.
Merry Christmas
Join the Discussion
You must be signed in to comment.