I am looking for a way to check at runtime which overrides of GetHashCode
do nothing else than to simply call a particular static method. I'm close, but there are minor differences that I cannot explain yet.
On the MethodInfo
objects of the overrides in question I call GetMethodBody().GetILAsByteArray()
. The idea is to compare those implementations to a set of likely candidates. A GetHashCode
override that simply calls our static method tends to look like one of three possibilities:
// Option 1 - Expression-bodied
public override int GetHashCode() => StaticClass.StaticMethod();
// Option 2 - Method-bodied
public override int GetHashCode()
{
return StaticClass.StaticMethod();
}
// Option 3 - Method-bodied with intermediate var
public override int GetHashCode()
{
var hashCode = StaticClass.StaticMethod();
return hashCode;
}
For simplicity, we will only consider option 1, the expression-bodied implementation.
I have already established that the following do not affect the result:
- Namespace
- Access modifier
- Type nesting (i.e. types nested inside another type)
- Type sealing (
public [sealed] class
) - Method sealing (
public [sealed] override int GetHashCode
)
The test solution contains a console application, a class library (which also provides StaticClass.StaticMethod
), and a second class library. (Everything references the first class library, and the console application also references the second class library.)
I am comparing the identical, expression-bodied GetHashCode
overrides defined by the following classes:
Lib
(a class that lives in the same class library responsible forStaticClass.StaticMethod
)OtherLib
(a class that lives in the other class library)ConsoleApplication
(a class that lives in the console application)LibGenerated
(a class generated at runtime by the first class library, into new assemblyAssemblyA
).Lib2Generated
(a class generated at runtime by the other class library, into new assemblyAssemblyB
).ConsoleApplicationGenerated
(a class generated at runtime by the console application, into new assemblyAssemblyC
).
Here are the byte arrays of the corresponding implementations in Debug mode:
Lib: 02|40|19|00|00|06|42
OtherLib: 02|40|13|00|00|10|42
ConsoleApplication: 02|40|14|00|00|10|42
LibGenerated: 02|40|01|00|00|10|42
Lib2Generated: 02|40|01|00|00|10|42
ConsoleApplicationGenerated: 02|40|01|00|00|10|42
Here they are in Release mode (where the only difference is that ConsoleApplication
now matches OtherLib
):
Lib: 02|40|19|00|00|06|42
OtherLib: 02|40|13|00|00|10|42
ConsoleApplication: 02|40|13|00|00|10|42
LibGenerated: 02|40|01|00|00|10|42
Lib2Generated: 02|40|01|00|00|10|42
ConsoleApplicationGenerated: 02|40|01|00|00|10|42
Looking at the results, we can note the following:
- The hardcoded type in the same assembly as
StaticClass.StaticMethod
is the only one that has a different second-to-last byte. - In Debug mode, the third byte is different for each of the hardcoded types, and they differ from the generated types as well.
- In Release mode, the third byte is different for the assembly containing
StaticClass.StaticMethod
, whereas the other hardcoded implementations have the exact same method bodies. - The generated types have the exact same method bodies (even though they, too, live in different assemblies, with differently named modules).
Naturally, Release mode is of primary interest here.
My questions are as follows:
- What explains the differences in the IL method bodies?
- How can I determine at runtime, in a somewhat reliable way, whether the implementations do the same thing?
Update based on comments by thehenny:
The call from the assembly that contains StaticClass.StaticMethod
is different because it uses MethodDef
(which apparently can be used to call a method in the same assembly), whereas external assemblies use MethodRef
.
Additional question: Let's say the StaticClass
is in a NuGet package, and the logic to test methods for a particular implementation is in there as well. Now this package needs an example implementation to compare methods to. However, it cannot define that by itself, as it will differ (because of MethodDef
). It can't reference a subdependency either, because to access StaticClass.StaticMethod
would need to access it in return, creating a circular reference. If we could create an assembly at runtime that produces the MethodRef
result similar to that of a hardcoded implementation in a separate assembly, this would solve the problem. How can do achieve that? Or actually, it is the third byte that differs here, so the difference might be in another detail entirely. What is it?