Wednesday, September 15, 2010

C# Compiler Bug, or just something obscure and frustrating?

Earlier today, I started getting the strangest error message when trying to run my tests.  The message varied a little depending on what test runner was being used, but the gist of the error message was:

"Could not load file or assembly or one of its dependencies.  Signature missing argument. (Exception from HRESULT: 0x801312E3)"

The Visual Studio solution this started occurring in is pretty simple.  It contains four (4) C# 4.0 class libraries, and two .NET 4.0 test projects.  Both test projects are using NUnit 2.5.7 and Rhino Mocks 3.6. One test project was working just fine, but when I tried to run the second test project's tests, I would get this error.  It occurred no matter if I ran it through ReSharper or through the NUnit GUI.

After three hours of trial and error, I finally found that the error appeared to be with a reference to the project containing the domain objects.  To better illustrate, here is the project hierarchy:

Test Project
    Class Library A
        Domain Project
        Class Library B
            Domain Project
        Class Library C
    Class Library B
        Domain Project
    Class Library C

It's not the most straightforward tree, but not complex by any means, and compiles without error or warning.  Yet for some reason the test runners were very upset that Class Library B was referencing the Domain Project.  It appeared to be at least in some way related to Rhino Mocks - when I removed the lines of code in the Test Project that included calls to the Expect() method, but left the actual project hierarchy the same, the error went away.  (The tests of course were then useless, so this wasn't a viable alternative, but it got me closer to finding the problem.)

The tests themselves are setting up expectations on a method from Class Project B that has a class from Domain Project as its return type.  You may notice, however, that Test Project does not reference Domain Project.  Because this solution is relatively new, so far the tests just verify that the method is called with the right inputs; I haven't yet written the tests to verify output.  In other words, I don't have to deal directly with the class from Domain Project yet, so I haven't referenced that project.

It eventually turned out that this was in fact the problem.  When I added a reference to Domain Project to Test Project, the errors went away and I was able to run my tests again. It seems that Rhino Mocks requires direct references to all the types employed by a method signature, even if the C# compiler doesn't need them all to build the DLL.  It makes it so the error ends up in a bizarre no-man's land - it's not a compile-time problem, but it manifests when the DLL is being loaded, which is before what we typically think of being run-time.

I'm not one who understands compiler design and implementation very well, so it's hard for me to say what the compiler's doing here. From comparing disassembles of the DLL compiled with and without the Domain Project reference, though, it's pretty clear that without the project reference, the compiler doesn't know the return type of delegate passed into Expect(), and can't build the method signatures correctly.  (See the footnotes for more detail.)

Honestly, this feels like something the compiler should at least send up a warning about, or perhaps even fail to build on. It results in a compiled DLL that can't be used; it feels like it allows us to create invalid binaries.  Maybe detecting this kind of problem would be so enormously complex that its better to put the burden on the developer, but you'd think they'd have better documented it in that case.

So the final take-away is: when using generics and/or delegation, make sure all types implicitly referenced by your code are explicitly referenced in the project References.

This is a very remote and unusual case, but I could find absolutely nothing on Google or in any Microsoft documentation that gave any hints, and the error message itself was basically useless.  So I am putting this recap out on the Internet in the hopes that if anyone else ever runs into this, they'll have a little more insight than I did.

Footnotes

This is the C# code written:

[Test]
public void MyTest()
{
  _classFromProjectB
    .Expect(x => x.GetBatch(Arg<int>.Is.Anything, Arg<DateTime>.Is.Anything));

  // invoke the method being tested
}

Without the Domain Project reference, this is what the compiler produces:

[CompilerGenerated]
private static byte CS$<>9__CachedAnonymousMethodDelegate1;

[CompilerGenerated]
private static IClassFromProjectB <MyTest>b__0(void x)
{
  byte CS$1$0000 =
    (byte)x.GetBatch(Arg<int>.Is.Anything, Arg<DateTime>.Is.Anything);
  return (IClassFromProjectB) CS$1$0000;
}

[Test]
public void MyTest()
{
  if (CS$<>9__CachedAnonymousMethodDelegate1 == 0)
  {
    CS$<>9__CachedAnonymousMethodDelegate1 =
      (byte) new int(null, (IntPtr) <MyTest>b__0);
  }
  this._classFromProjectB.Expect<IClassFromProjectB, byte>(
     (Function<IClassFromProjectB, byte>) CS$<>9__CachedAnonymousMethodDelegate1);

  // invoke the method being tested
}

With the project reference, it produces:

[Test]
public void MyTest()
{
  this._classFromProjectB
    .Expect<IClassFromProjectB, List<DomainObject>>(
      delegate (IClassFromProjectB x) 
      {
        return x.GetBatch(Arg<int>.Is.Anything, Arg<DateTime>.Is.Anything);
      });

  // invoke the method being tested
}

3 comments:

  1. Thanks for the post Nate, I just ran into the same issue.

    ReplyDelete
  2. Just ran into the same issue where I didn't have a reference to a dll for an object that a Rhino mock object was returning (even when I set the return object to null). Adding the reference fixed the problem, thanks for saving me the time to figure it out!

    ReplyDelete
  3. Thanks for this. I took a while to find your post as I only got the error message you posted when running MSTest from command line. I was running test from Resharper and was getting a"SettingsException" - "Incompatible platform settings 'Default' with system architecture 'X64'"
    Hopefully by adding this to comments others will find the answer quicker.
    I fixed by adding references into the test project.

    ReplyDelete