Versioning and Aliases with C#

A software project rarely exists as a single version of code that’s never revised, unless the soft­ware never sees the light of day. In most cases, the software library writer is going to want to change some things, and the client will need to adapt to such changes.

Dealing with such issues is known as versioning, and it’s one of the hardest tasks to do in software. One reason why it’s tough is that it requires a bit of planning and foresight; you have to determine the areas that might change and modify the design to allow change.

Another reason why versioning is tough is that most execution environments don’t provide much help to the programmer. In C++, compiled code has internal knowledge of the size and layout of all classes burned into it. With care, you can make some revisions to the class without forcing all users to recompile, but the restrictions are fairly severe. When compatibility is broken, all users need to recompile to use the new version. This may not be that bad, but installing a new version of a library may cause other applications that use an older version of the library to cease functioning.

Although it’s still possible to write code that has versioning problems, .NET makes versioning easier by deferring the physical layout of classes and members until JIT compilation time. Rather than providing physical layout data, a .NET assembly provides metadata that allows a type to be laid out and accessed in a manner that makes sense for a particular process architecture.

1. A Versioning Example

The following code presents a simple versioning scenario and explains why C# has new and override keywords. The program uses a class named Control, which is provided by another company.

public class Control

{

}

public class MyControl: Control

{

}

During implementation of MyControl, a developer can add the virtual function Foo():

public class Control

{

}

public class MyControl: Control

{

public virtual void Foo() {}

}

This works well, until an upgrade notice arrives from the suppliers of the Control object. The new library includes a virtual Foo() function on the Control object:

public class Control {

// newly added virtual public virtual void Foo() {}

}

public class MyControl: Control {

public virtual void Foo() {}

}

Control uses Foo() as the name of the function, but this is only a coincidence. In the C++ world, the compiler will assume that the version of Foo() in MyControl does what a virtual over­ride of the Foo() in Control should do and will blindly call the version in MyControl. This is bad.

In the Java world, this will also happen, but things can be a fair bit worse; if the virtual function doesn’t have the same return type, the class loader will consider the Foo() in MyControl to be an invalid override of the Foo() in Control, and the class will fail to load at runtime.

In C# and the .NET runtime, a function defined with virtual is always considered to be the root of a virtual dispatch. If a function is introduced into a base class that could be considered a base virtual function of an existing function, the runtime behavior remains the same. When the class is next compiled, however, the compiler will generate a warning, requesting that the programmer specify their versioning intent. Returning to the example, to use the default behavior of not considering the function an override, add the new modifier in front of the function:

class Control {

public virtual void Foo() {}

}

class MyControl: Control {

// not an override

public new virtual void Foo() {}

}

The presence of new will suppress the warning.

If, on the other hand, the derived version is an override of the function in the base class, use the override modifier:

class Control {

public virtual void Foo() {}

}

class MyControl: Control {

// an override for Control.Foo()

public override void Foo() {}

}

This tells the compiler the function really is an override.

2. Coding for Versioning

The C# language provides some assistance in writing code that versions well:

  • Methods, for example, aren’t virtual by default. This helps limit the areas where versioning is constrained to those areas that were intended by the designer of the class and prevents “stray virtuals” that constrain future changes to the class.
  • C# also has lookup rules designed to aid in versioning. Adding a new function with a more specific overload (in other words, one that matches a parameter better) to a base class won’t prevent a less-specific function in a derived class from being called, so a change to the base class won’t break existing behavior.

A language can do only so much. That’s why versioning is something to keep in mind when designing classes. One specific area that has some versioning trade-offs is the choice between classes and interfaces.

The choice between class and interface should be fairly straightforward. Classes are appro­priate only for “is-a” relationships (where the derived class is really an instance of the base class), and interfaces are appropriate for all others. If you choose to use an interface, however, good design becomes more important because interfaces simply don’t version; when a class implements an interface, it needs to implement the interface exactly, and adding another method later will mean that classes that thought they implemented the interface no longer do.

3. External Assembly Aliases

At the CLR level, a type reference is fully qualified by both a namespace and a full assembly name. This makes it possible to reference two types that have identical names and that exist in iden­tical namespaces, with the types being differentiated by the name of their assembly. While this scenario isn’t necessarily common, it can occur with in-house projects where developers have been loose with adding full namespace hierarchies or where versioning has been accom­plished by using the traditional Windows style of DLL versioning that involves including the version number in the binary’s filename. If you need to reference multiple versions of an assembly that has been versioned in this manner, using namespace qualifiers is insufficient to specify which version of the type should be used.

To solve this problem, C# 2.0 introduces the compile-time ability to specify an assembly alias for types so multiple types that exist in the same namespace can be used. A source code file that wants to employ this alias uses the extern alias statement to bring the alias into scope. This creates a new top-level namespace for the types from the aliased assembly, allowing two identically named types to be distinguished based on aliases.

To provide an example of using external assembly aliases, consider the two Math classes that have been developed independently at a particular company:

//in assembly maths.dll namespace AcmeScientific {

public class Math {

public int Calc() { return 0;}

}

}

//in assembly utils.dll namespace AcmeScientific {

public class Math {

public void DoSums() {}

}

}

If you needed to use functionality from the Math classes in both of these assemblies in previous versions of C#, you’d need to make changes to the type name or namespace hierarchy in one of these assemblies. To resolve this problem in C# 2.0, the assemblies that the Math classes live in are aliased as part of the /reference C# compiler option (aliases shown in italics):

csc /reference:Maths=maths.dll /reference:htils=utils.dll /t:exe /out:MyApp.exe source.cs

To use the aliases in code, add extern alias declarations to the top of the source code file and reference the types within the aliased assemblies using the alias name, a double colon, and then the full namespace name:

extern alias Maths; extern alias Utils;

namespace AcmeScientific.MyApp {

class App{

static void Main()

{

Maths::AcmeScientific.Math m = new Maths::AcmeScientific.Math();

m.Calc();

Utils::AcmeScientific.Math u = new Utils::AcmeScientific.Math();

u.DoSums();

}

}

}

Because the number of source code files that need to distinguish between types based on assembly aliases is likely to be relatively small, it’s possible to use the using statement to bring an aliased namespace into the global namespace for that file. In the previous example, if the Math type in the Maths assembly was the only one needed in a source file, the following code would be easier than qualifying the Math class with the full alias and namespace every time:

extern alias Maths;

using Maths::AcmeScientific;

namespace AcmeScientific.MyApp {

class App{

static void Main()

{

Math m = new Math();

m.Calc();

}

}

}

You can place a type in both the global namespace and an aliased assembly namespace by including it twice in the /references switch—once with an alias and once without. Using this technique, you can rewrite the previous snippet as follows:

//command line to compile

//csc /reference:maths.dll /reference:Maths=maths.dll

// /reference:Utils=utils.dll /t:exe /out:MyApp.exe source.cs

using AcmeScientific;

namespace AcmeScientific.MyApp {

class App{

static void Main()

{

Math m = new Math();

m.Calc();

}

}

}

Along the same theme, two assemblies can share the same alias as long as the types that need to be used within these assemblies can be differentiated based on their names (including the namespace). Types from assemblies that aren’t aliased are implicitly placed in the global namespace and can be fully qualified by using the global assembly alias. Incorporating this into the previous example, it becomes the following:

using global::AcmeScientific;

namespace AcmeScientific.MyApp {

class App {

static void Main()

{

Math m = new Math();

m.Calc();

}

}

}

The C# compiler will prevent using global as an assembly alias.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *