Figuring out mysterious `MissingMethodException` in a simple C# application
As we already know from C# Language Features vs. Target Frameworks you can use most of the latest C# language features targeting .Net Standard or Full Framework. Some features just work with any target frameworks, but some require special attributes or types to be defined during compilation.
Here is an interesting problem that I’ve faced recently that took quite a bit of time to figure out.
Core.csproj
Let’s say you have a core library that multi-targets netstandard2.0
and net8.0
. The library could have a bunch of stuff, like helpers for Span<T>
, or just anything else. For the sake of this example, this library just would have one class Config
type with an init-only
property:
// Core.csproj
// <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
namespace Core;
public class Config { public int X { get; init; } }
Obviously, the code won’t compile, since netstandard2.0 version doesn’t have IsExternalInit
type. The solution sounds pretty easy, right? We just add IsExternalInit.cs
file manually (or with some MSBuild magic) with the following content:
#if NETSTANDARD2_0
namespace System.Runtime.CompilerServices;
internal class IsExternalInit;
#endif
We either can add IsExternalInit.cs
conditionally to the project itself if the target is netstandard2.0
or just have #if NETSTANDARD2_0
inside of it. We can’t just add this type for all the targets, but in this case we could face a compilation errors if the Core
project would have InternalsVisibleTo
attribute for a test project that target net8.0
or any other target runtime that has IsExternalInit
type already defined.
Library.csproj
Now, we add another library, let’s say, Library.csproj
that targets only netstandard2.0
that uses our Core.csproj
. This might be not a super common case, but I’ve seen quite a few of them in the wild:
// Library.csproj
// <TargetFramework>netstandard2.0</TargetFramework>
public static class ConfigFactory
{
public static Config Create(int value) => new () { X = value };
}
Application.exe
And now we have a console app that targets net8.0
that just uses the factory:
// Application.exe
// <TargetFramework>net8.0</TargetFramework>
using Factory;
var config = ConfigFactory.Create(42);
Console.WriteLine("Done!");
Here is the dependency diagram:
Would you expect any issues with this code? Me neither, to be honest! But here is the output:
Unhandled exception. System.MissingMethodException: Method not found: 'Void Configuration.Config.set_X(Int32)'.
at Factory.ConfigFactory.Create(Int32 value)
at Program.<Main>$(String[] args) in Application/Program.cs:line 3
You can check the IL, and you’ll see that the set_X(Int32)
“method” (which is a property) is definitely exists in the Config
class. But why do we get the error? Is it a compiler bug? Not really!
The root cause
So here is the issue. Even though the Core.csproj
is multi-targeted, the question is: which version of Core.dll
is actually deployed in the output of folder? The core.dll
that targets .netstandard2.0 or the core.dll
that targets net8.0
? At runtime there is no such a thing as ‘multi-targeting’, the multi-targeting is a build-time feature!
Sine Application
project targets net8.0
and implicitly references Core.csproj
, the net8.0
version is deploy.
Is it a problem? Actually, yes, it is. Let’s check the IL for the ConfigFactory
:
.method public hidebysig static class [Core]Core.Config
Create(
int32 'value'
) cil managed
{
// [7 47 - 7 67]
IL_0000: newobj instance void [Core]Core.Config::.ctor()
IL_0005: dup
IL_0006: ldarg.0 // 'value'
IL_0007: callvirt instance void modreq ([Core]System.Runtime.CompilerServices.IsExternalInit) [Core]Core.Config::set_X(int32)
IL_000c: nop
IL_000d: ret
} // end of method ConfigFactory::Create
Library.csproj
targets netstandard2.0
and uses System.Runtime.CompilerServices.IsExgternalInit
type from Core.dll
, but at runtime we have Core.dll
that targets net8.0
with the following set_X
property:
.property instance int32 X()
{
.get instance int32 Core.Config::get_X()
.set instance void modreq ([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) Core.Config::set_X(int32)
} // end of property Config::X
I.e. the one, that takes IsExternalInit
from System.Runtime
dll and not Core
assembly. Yes, you could have the same types defined in different assemblies, and from the runtime point of view, they’re definitely are the two different types.
Solutions to the issue
So, how can we solve this issue?
The simplest solution is just to use a tool that solved this problem already, for instance, PolySharp
nuget package. But if this is not an option for you for some reason, there are two solutions available.
First, you can add IsExternalInit
unconditionally. This might cause a problem with InternalsVisibleTo
as I mentioned before, and second solution is based on TypeForwardingAttribute
:
#if NETSTANDARD2_0
namespace System.Runtime.CompilerServices;
internal class IsExternalInit;
#else
[assembly: global::System.Runtime.CompilerServices.TypeForwardedTo(
typeof(global::System.Runtime.CompilerServices.IsExternalInit))]
#endif
TypeForwardedToAttribute
tells the runtime where to look the types that supposed to be in the current assembly. In this case, for net8.0
case we’re telling the runtime that IsExternalInit
class is located in BCL and everything works just fine. Btw, this is the solution that PolySharp
library uses under the hood as well.