1

Setup

I am working on a C# project without external dependencies, named "Hopper". I have split it up into a couple of different modules, each contained within a corresponding sub-folder. The relevant ones for the problem are the following:

  • Utils/Hopper.Utils.csproj
  • Shared/Hopper.Shared.csproj
  • Core/Hopper.Core.csproj, which references Utils and Shared, more details later
  • Mine/Hopper.Mine.csproj, which references Core

All of these target .NET 4.8, including Hopper.Core.

I have set the AssemblyName property for each of the projects to the corresponding string. For example, the Hopper.Utils.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Library</OutputType>
    <TargetFramework>net4.8</TargetFramework>
    <AssemblyName>Hopper.Utils</AssemblyName>
  </PropertyGroup>

</Project>

Hopper.Core.csproj references the other projects via ProjectReference, like so:

<ItemGroup>
  <ProjectReference Include="..\Utils\Hopper.Utils.csproj" />
  <ProjectReference Include="..\Shared\Hopper.Shared.csproj" />
</ItemGroup>

Hopper.Mine.csproj, created for testing purposes, references core like this:

<ItemGroup>
  <ProjectReference Include="..\Core\Hopper.Core.csproj" />
</ItemGroup>

The Hopper.Core project contains some types, both classes and structs. Some source files have been autogenerated by a tool and contain #pragma warning disable at the top. (Removing it did not help).

The two projects referenced by Hopper.Core just contain some types and attributes, used by Core, so nothing of interest to the problem. They do not reference any other projects or dll's.

The problem

I am able to compile Hopper.Mine by running dotnet build in the sub-folder with the project file. I am able to see the output dll's of the referenced sub-projects in bin/Debug/net4.8/Hopper.Whatever.dll under each project's folder. Intellisense in VSCode also works correctly and does not report any errors.

Hopper.Mine has a single class with a Main function, created for a test. It lists the types of Hopper.Core that have loaded successfully, and reports the ones that have not been loaded:

using System;
using System.Reflection;
using System.Text;

namespace Hopper.Mine
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Assembly lib = typeof(Hopper.Core.Action).Assembly;
            Type[] types;
            try
            {
                types = lib.GetTypes();
            }
            catch (ReflectionTypeLoadException ex)
            {
                StringBuilder sb = new StringBuilder();
                foreach (Exception exSub in ex.LoaderExceptions)
                {
                    Console.WriteLine(exSub.Message);
                }
                types = ex.Types;
            }
            
            foreach (Type type in types)
            {
                if (type != null)
                    Console.WriteLine(type.FullName);
            }
        }
    }
}

Running this code, I can see a bunch of problematic types, some of which repeat, and the rest of types that have loaded successfully:

Could not load type 'Hopper.Core.Stat.Attack' from assembly 'Hopper.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Could not load type 'Hopper.Core.Stat.Dig' from assembly 'Hopper.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Could not load type 'Hopper.Core.Stat.Move' from assembly 'Hopper.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Could not load type 'Hopper.Core.Stat.Push' from assembly 'Hopper.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Could not load type 'Hopper.Core.Stat.Attack' from assembly 'Hopper.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
Could not load type 'Hopper.Core.Stat.Attack' from assembly 'Hopper.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

... more errors here ...

Hopper.Core.DirectedPredict
Hopper.Core.UndirectedPredict
Hopper.Core.DirectedDo

... more types here ...

Hopper.Core.Stat.Attack+Source+Resistance
Hopper.Core.Stat.Push+Source+Resistance

To be noted 4 things:

  1. The types that failed to load are all structs and derive from IStat.
  2. The source files with these types have been autogenerated by a tool (other files generated by the tool load without errors).
  3. The types do not reference any types from Hopper.Shared or Hopper.Utils.
  4. Some of the types have nested structs, up to 3 level deep, some of the deeper ones seem to have loaded successfully.

I am pretty sure that the dll output from the Hopper.Core's output folder is identical to the one in the output folder of Hopper.Mine. I am also pretty sure that the right dll is linked against when I start the program (I've tried debugging with VS, where I can see which dll is being linked against and the paths matched).

I have tried removing AssemblyName properties, renaming csproj files from Hopper.Whatever.csproj to Hopper_Whatever.csproj, I've obviously tried cleaning up any previous output dll's and building from scratch, none of which helped. I have also been able to reproduce this on a different machine with the exact same results.

I have tried looking at the Hopper.Core dll with a decompiler, ILSpy. I am able to see the "missing" types as they are in code. ILSpy decompilation result

I have searched on google for hours for a problem like mine, to no avail. I have found many cases of people setting up their project files incorrectly, due to which they unexpectedly link against a different version of the dll than they expected. For some, such dependency showed up in requirements of one of the referenced third party projects. Some blame GAC for providing incorrect version of the dll. However, I'm pretty sure that my projects are set up right (+ the set up is pretty simple and no third party projects are being referenced) and I am referencing the right dll's (since I can see my types in it). It kind of seems like the types are 'declared' in the dll, but not 'defined' (sort of), so the dll's turn out corrupted. But then why would the decompilation not fail? So I'm currently completely stuck on this.

Loading the assembly manually

I have tried linking against the dll's dynamically, by their names. Listing the types imported from Hopper.Shared and Hopper.Utils works, however, when I try it with Hopper.Core, I get the same output as the example above.

To achieve this, I have copied the previous dll's from the output over to a new folder, removed the reference from Hopper.Mine, compiled the new code with dotnet build, after which moved the executable to the new folder and finally ran it in the console. The code:

using System;
using System.Reflection;
using System.Text;

namespace Hopper.Mine
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Assembly shared = Assembly.LoadFrom("Hopper.Shared.dll");
            Assembly utils = Assembly.LoadFrom("Hopper.Utils.dll");
            Assembly core = Assembly.LoadFrom("Hopper.Core.dll");
            Type[] types;
            try
            {
                types = core.GetTypes();
            }
            catch (ReflectionTypeLoadException ex)
            {
                StringBuilder sb = new StringBuilder();
                foreach (Exception exSub in ex.LoaderExceptions)
                {
                    Console.WriteLine(exSub.Message);
                }
                types = ex.Types;
            }
            
            foreach (Type type in types)
            {
                if (type != null)
                    Console.WriteLine(type.FullName);
            }
        }
    }
}

To reproduce

If it helps, you may try running my code. Here's the github link to the current state of the project. In order to run it, you need:

  1. .NET 4.8 installed.
  2. Clone the repository from the link above.
  3. Some code is still missing. You need to run the project in the folder Meta to generate it. Just run dotnet run in the Meta folder.
  4. Now you can run the Mine subproject.
  • The Core project should be built with Net 4.8 and then all projects should be target to the Core version you want. I've been seeing lots of similar issues in last two week with target Core 3.1. See following : https://stackoverflow.com/questions/67316994/using-net-core-3-1-c-sharp-dll-in-c-project?force_isolation=true#comment118992422_67316994 – jdweng Apr 30 '21 at 15:21
  • @jdweng, I'm not targeting Core 3.1, though. All of the projects mentioned in my question target .NET 4.8 alone. – Anton Curmanschii Apr 30 '21 at 15:40
  • What about Core/Hopper.Core.csproj, which references Utils and Shared, more details later. do you need to recompile as Net 4.8? – jdweng Apr 30 '21 at 15:47
  • @jdweng, all mentioned projects have `TargetFramework` set to `net4.8` like in the example for `Hopper.Utils.csproj`. `Core` is not named `Core` because it targets dotnet Core, if that is what you mean. – Anton Curmanschii Apr 30 '21 at 15:57
  • Do you have dotnet Core installed? – jdweng Apr 30 '21 at 16:49
  • @jdweng, I do have version Core 3.1.1, but I'm not using it to build the project, so I do not see why it would matter. Btw, compiling with `TargetNetwork` set to netcoreapp3.1 produces the same output as with .NET Framework 4.8, which I've been using initially. – Anton Curmanschii Apr 30 '21 at 17:54
  • Does code work on build machine with Net 4.8 ass target? Core 3.1.1 is from last january. See : https://dotnet.microsoft.com/download/dotnet/3.1 – jdweng Apr 30 '21 at 18:44
  • @jdweng it used to work with any .NET (core, not core, whatever else there is) before I have added the code generation and have done a massive refactoring over the entire project. I do not understand your question. So what does it matter that 3.1.1 is from January? It used to work back in January. It has to have something to do with the actual types in the project. And I *am* able to *build* it, it does not *run* correctly. Please see the actual problem description in the question. – Anton Curmanschii Apr 30 '21 at 19:52
  • The error is due to the compile version and the run version being different. Your 3.1.1 is over a year old. Does code run in Net 4.8 and target 4.8? – jdweng Apr 30 '21 at 22:00
  • @jdweng yes, the code does run, either compiled with net 4.8 or core 3.1, producing the results shown in the question. – Anton Curmanschii Apr 30 '21 at 22:20

1 Answers1

0

Found out the reason. There is a bug in the CLR type system that has been around for 6 years. Basically, fields (either static or non-static) of type of a generic struct declared in other structs cause this behavior. See this github thread for more details.

In my case, I have been able to make a shorter version that illustrates the bug.

public struct Index<T>
{
}

public struct Test
{
    public static Index<Test> Index;
}

Use the following code to test the program:

public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine(typeof(Test).Assembly.GetTypes());
    }
}

When the code is run, the following error is thrown:

Unhandled Exception: System.TypeLoadException: Could not load type 'Hopper.Mine.Test' from assembly 'Hopper.Mine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
   at Hopper.Mine.Program.Main(String[] args)

However, there is an easy sort of workaround for this. Changing Test from struct to class works:

// This works!
public struct Index<T>
{
}

public class Test
{
    public static Index<Test> Index;
}

That may not be acceptable in some cases. So here is another workaround — storing the static field in an array and accessing it with a property also works:

public struct Index<T>
{
}

public struct Test
{
    public static Index<Test> Index { get => index[0]; set => index[0] = value; }
    public static Index<Test>[] index = new Index<Test>[1];
}