Trying to understand method signature changes spanning assemblies

We ran into a strange problem with a promotion and I'm hoping I'll be able to explain it with code. I want to understand why it behaves in the manner it is.

Assembly 1

public static class Foo
{
    public static string DoStuff()
    {
        // Do something

        return "some string";
    }
}

Assembly 2:

public class Bar
{
    public void SomeMethod()
    {
        // I realize the below is not what should be done for catching exceptions, as the exception is never thrown due to the return, and seems unnecessary either way... 
        // this is inherited code and has not been modified to correct.

        try
        {
            var someValue = Foo.DoStuff();
        }
        catch (Exception)
        {
            return;
            throw;
        }
    }
}

Requirements changed so that DoStuff would need to take in a parameter, the value of which would change the behavior slightly. Note that only assembly 1.Foo is changing.

New Foo

public static class Foo
{
    public static string DoStuff(bool someBool = false)
    {
        // Do something

        return "some string";
    }
}

This recompiled fine, and Assembly 2 was able to successfully utilize the changed method signature without complaint. My check in was performed, and project dlls that had changes were promoted (note this was only Assembly 1 dll).

After promotion we found out that Assembly 2 was failing on the Foo.DoStuff() call - unfortunately I cannot provide an exception as the above code was swallowing it.

Even though no actual code changed in Assembly 2, it did seem to have an impact on the dll on recompile, even though the method signature - at least in my mind - is the same due to providing a default value for the new parameter.

I used dotnet peek in order to peek at the "old dll" and the "new dlls" (even though no code change to that assembly and did note a difference in the two DLLs in that in the old DLL, the default value parameter was not supplied, but in the recompiled, the default value parameter was supplied.

I guess my question(s) boil down to this:

  1. Why does the compiled code on assembly 2 change based on a method signature that (at least I think) should be transparent?
  2. Would the method of avoiding a situation like this just be "deploy everything"? Rather than trying to deploy based on code changes? Note that DLLs are not checked in, so there was no actual "code change" to assembly 2, though the DLL was different based on the changes to Assembly 1.
Jon Skeet
people
quotationmark

Why does the compiled code on assembly 2 change based on a method signature that (at least I think) should be transparent?

No, it shouldn't. When you don't specify an argument to correspond with an optional parameter, the default value is provided at the call site - i.e. assembly 2 in your case. That's how optional parameters work in C#. It's a pain, but that's life.

Would the method of avoiding a situation like this just be "deploy everything"?

Yes. Basically, the meaning of the source in assembly 2 has changed even though the code itself hasn't - so the compiled code has changed, and it needs to be redeployed.

You can see subtle breaking changes like this in other cases, too - where the same old "client" code compiles against new "receiving" code, but has a different meaning. Two examples:

  • Suppose you change a method signature from Foo(long x) to Foo(int x) but at the call site, you only call it as Foo(5)... that compiles in both cases, but to different code.

  • Suppose you have change a method signature from Foo(int x, int y) to Foo(int y, int x) and you have a call of Foo(x: 5, y: 2)... again, the meaning of the code changes from Foo(5, 2) to Foo(2, 5), if you see what I mean. Recompiling the unchanged source code against the new "receiving" code will change the behaviour.

  • Suppose you have a constant in assembly 1, e.g. public const string Foo = "Foo";. If you change that to public const string Foo = "Bar", but don't recompile assemblies using the constant, those assemblies will still use a value of "Foo". (This goes for default values of optional parameters, too.)

people

See more on this question at Stackoverflow