JustPaste.it

Referencing constants across assemblies, that may not get updated together.

Here is an article i wrote in 2007, pasted whole sale

Referencing Constants

We all know the classic lesson from school. Never use “magic numbers” in your code. Always define a constant and use it throughout. Not only does it give it contextual meaning, it also makes it easy to alter the value only at one place in the future. Sweet deal huh? Well, maybe not as much as one might think. There is a subtle issue with the use of constants that perhaps not everybody is aware of. Let’s do something practical to sink the idea; go ahead and open up Visual Studio:

  1. Create a class library project, call it ConstantLibrary
  2. Create a WinForms project, call it ConstantDependent
  3. Let’s imagine for a moment that we’re going to program World of Warcraft all over again. :-)

Those who play WOW know that the maximum attainable player level used to be 60. So let’s create a PlayerLimits class in ConstantLibrary

namespace ConstantExample { public class PlayerLimits { public const int MaxLevel = 60; } }

Now, in ConstantDependent, 1. Use Form1 2. Put in a button btnMaxLevel 3. Put in a label lblMaxLevel 4. Set the btnMaxLevel click event to

private void btnMaxLevel_Click(object sender, EventArgs e) { this.lblMaxLevel.Text = PlayerLimits.MaxLevel.ToString(); }

Build and run the solution. When you click the button, 60 appear. Now,

  1. Go back and adjust PlayerLimits.MaxLevel = 70, the new level limit introduced in The Burning Crusade expansion.
  2. Build only the ConstantExample project, and copy its new assembly to ConstantDependent’s bin/Debug directory to overwrite the old assembly.
  3. Run the ConstantDependent.exe that is directly there; make sure you did not recompile it.
  4. Go ahead and press the button again.

It remains at 60. Oops. What is happening here?

  1. Launch MSIL DASM, the disassembler supplied with the .NET Framework SDK.
  2. Load ConstantDependent.exe into it.
  3. Look for method btnMaxLevel_Click and open it up, and look at the line with the ldc instruction to load an integer value onto the stack.

To be specific, it would be IL_0007 in the sample below.

.method private hidebysig instance void btnMaxLevel_Click(object sender, class [mscorlib]System.EventArgs e) cil managed { // Code size 24 (0x18) .maxstack 2 .locals init ([0] int32 CS$0$0000) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldfld class [System.Windows.Forms]System.Windows.Forms.Label ConstantExample.Form1::lblMaxLevel IL_0007: ldc.i4.s 60 IL_0009: stloc.0 IL_000a: ldloca.s CS$0$0000 IL_000c: call instance string [mscorlib]System.Int32::ToString() IL_0011: callvirt instance void [System.Windows.Forms System.Windows.Forms.Control::set_Text(string) IL_0016: nop IL_0017: ret } // end of method Form1::btnMaxLevel_Click

The IL code is using the literal integer value of 60. Ouch. What the C# compiler has done is to inline the constant value literally into the client assembly. If you are in one of those environments where you are only allowed to promote changed assemblies into UAT or production environment, and you thought you could alter just an assembly with modified constants, well, we all thought wrong.

Recommendation: Use constants only within an assembly. If they are placed in some other assembly, make sure they get compiled together and promoted together, even when the client assembly has no change in code. If you can guarantee the constants never change values, then power to you. Otherwise, use static read-only values for dynamic referencing. The following snippet will “propagate” the correct value to the client assembly even if it wasn’t compiled together.

public class RaidLimits { public static readonly int MaxPlayers = 25; }