Question

Can anyone explain the following behavior?

In summary, if you create multiple CLS compliant libraries in Visual Studio 2008 and have them share a common namespace root, a library referencing another library will require references to that library's references even though it doesn't consume them.

It's pretty difficult to explain in a single sentence, but here are steps to reproduce the behavior (pay close attention to the namespaces):

Create a library called LibraryA and add a a single class to that library:

namespace Ploeh
{
    public abstract class Class1InLibraryA
    {
    }
}

Make sure that the library is CLS Compliant by adding [assembly: CLSCompliant(true)] to AssemblyInfo.cs.

Create another library called LibraryB and reference LibraryA. Add the following classes to LibraryB:

namespace Ploeh.Samples
{
    public class Class1InLibraryB : Class1InLibraryA
    {
    }
}

and

namespace Ploeh.Samples
{
    public abstract class Class2InLibraryB
    {
    }
}

Make sure that LibraryB is also CLS Compliant.

Notice that Class1InLibraryB derives from a type in LibraryA, whereas Class2InLibraryB does not.

Now create a third library called LibraryC and reference LibraryB (but not LibraryA). Add the following class:

namespace Ploeh.Samples.LibraryC
{
    public class Class1InLibraryC : Class2InLibraryB
    {
    }
}

This should still compile. Notice that Class1InLibraryC derives from the class in LibraryB that doesn't use any types from LibraryA.

Also notice that Class1InLibraryC is defined in a namespace that is part of the namespace hierarchy defined in LibraryB.

So far, LibraryC has no reference to LibraryA, and since it uses no types from LibraryA, the solution compiles.

Now make LibraryC CLS compliant as well. Suddenly, the solution no longer compiles, giving you this error message:

The type 'Ploeh.Class1InLibraryA' is defined in an assembly that is not referenced. You must add a reference to assembly 'Ploeh, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

You can make the solution compile again in one of the following ways:

  • Remove CLS Compliance from LibraryC
  • Add a reference to LibraryA (although you don't need it)
  • Change the namespace in LibraryC so that it is not part of LibraryB's namespace hierarchy (e.g. to Fnaah.Samples.LibraryC)
  • Change the namespace of Class1InLibraryB (that is, the one not used from LibracyC) so that it is does not lie in LibraryC's namespace hierarchy (e.g. to Ploeh.Samples.LibraryB)

It seems that there is some strange interplay between the namespace hierarchy and CLS compliance.

Solving this issue can be done by picking one of the options in the list above, but can anyone explain the reason behind this behavior?

Was it helpful?

Solution

I had a look into the official documents for the CLS (http://msdn.microsoft.com/en-us/netframework/aa569283.aspx), but my head exploded before I could find a simple answer.

But I think the basis is that the compiler, in order to verify the CLS compliance of LibraryC, needs to look into possible naming conflicts with LibraryA.

The compiler must verify all "parts of a type that are accessible or visible outside of the defining assembly" (CLS Rule 1).

Since public class Class1InLibraryC inherits Class2InLibraryB, it must verify the CLS compliance against LibraryA as well, in particular because "Ploeh.*" is now "in scope" for CLS Rule 5 "All names introduced in a CLS-compliant scope shall be distinct independent of kind".

Changing either the namespace of Class1InLibraryB or Class1InLibraryC so they become distinct seems to convince the compiler there is no chance for a name conflict anymore.

If you choose option (2), add the reference and compile, you'll see that the reference is not actually marked in the resulting assembly meta-data, so this is a compilation/verification-time dependency only.

OTHER TIPS

Remember that the CLS is a set of rules that apply to generated assemblies and is designed to support interoperability between languages. In a sense, it defines the smallest common subset of rules that a type must follow to ensure that it is language and platform agnostic. CLS-compliance also only applies to items that are visible outside of their define assembly.

Looking at some of the guidelines CLS-compliant code should follow:

  • Avoid the use of names commonly used as keywords in programming languages.
  • Not expect users of the framework to be able to author nested types.
  • Assume that implementations of methods of the same name and signature on different interfaces are independent.

The rules for determining CLS-compliance are:

  • When an assembly does not carry an explicit System.CLSCompliantAttribute, it shall be assumed to carry System.CLSCompliantAttribute(false).
  • By default, a type inherits the CLS-compliance attribute of its enclosing type (for nested types) or acquires the level of compliance attached to its assembly (for top-level types).
  • By default, other members (methods, fields, properties, and events) inherit the CLS-compliance of their type.

Now, as far as the compiler is concerned, (CLS Rule 1) it must be able to apply the rules for CLS-compliance to any information that will be exported outside the assembly and considers a type to be CLS-compliant if all its publicly accessible parts (those classes, interfaces, methods, fields, properties, and events that are available to code executing in another assembly) either

  • have signatures composed only of CLS-compliant types, or
  • are specifically marked as not CLS-compliant.

By CTS rules, a scope is simply a group/collection of names and within a scope a name may refer to multiple entities as long as they are of different kinds (methods, fields, nested types, properties, events) or have different signatures. A named entity has its name in exactly one scope so in order to identify that entry both a scope and a name must be applied. The scope qualifies the name.

Since types are named, the names of types are also grouped in to scopes. To fully identify a type, the type name must be qualified by the scope. Types names are scoped by the assembly that contains the implementation of that type.

For scopes which are CLS-compliant, all names must be distinct independent of kind, except where the names are identical and resolved via overloading. In otherwords, while the CTS allows a single type to use the same name for a field and a method, the CLS does not (CLS Rule 5).

Taking this one step further, a CLS-compliant type must not require the implementation of non-CLS-compliant types (CLS Rule 20) and must also inherit from another CLS-complaint type (CLS Rule 23).

An assembly can depend on other assemblies if the implementations in the scope of one assembly reference resources that are scoped in or owned by another assembly.

  • All references to other assemblies are resolved under the control of the current assembly scope.
  • It is always possible to determine which assembly scope a particular implementation is running in. All requests originating from that assembly scope are resolved relative to that scope.

What all of this ultimately means is that in order to verify-CLS compliance of a type, the compiler must be able to verify that all public parts of that type are also CLS-compliant. This means that it must ensure that the name is unique within a scope, that it does not depend on non-CLS-compliant types for parts of its own implementation and that it inherits from other types that are also CLS-compliant. The only way for it to do so is by examining all of the assemblies that the type references.

Remember that the build step in Visual Studio is essentially a GUI wrapper around executing MSBuild, which ultimately is nothing more than a scripted way to call the C# command line compiler. In order for the compiler to verify CLS-compliance of a type, it must know of and be able to find all of the assemblies that type references (not the project). Since it is called through MSBuild and ultimately Visual Studio, the only way for Visual Studio (MSBuild) to inform it of those assemblies is by including them as references.

Obviously, since the compiler is able to figure out that it is "missing" references in order to verify CLS-compliance and compile successfully, it would have been nice if it could have simply included those reference automatically on our behalf. The problem here is in determining which version of the assembly to include and where that assembly is on the file system. By forcing the developer to provide that information, the compiler helps guarantee that it is given the correct information. This also has the side-effect of ensuring that all dependent assemblies are copied to the Debug/bin or Release/bin folders during the build so they are in the correct directory when the application is run after it has been compiled.

The issue is fixed in Roslyn, which is available in Visual Studio 14.
As of July 2014, the current CTP is available here.
See this bug report for details.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top