CLSCompliant(true) drags in unused references
Asked Answered
R

3

29

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?

Roundhouse answered 10/8, 2009 at 10:12 Comment(3)
+1 i noticed it too, but had no time to open a question.Caryncaryo
Very interesting. I've never stumbled across this though because I've never used the same identical namespace across two class library projects.Trifling
Hit it too, and opened a question independently before finally discovering this one. I filed a bug as suggested in the other question. If you have any new input on the matter, I'd be interested as well.Perorate
N
19

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.

Neeley answered 10/8, 2009 at 16:47 Comment(3)
You are probably onto something, but I still don't understand it. Why does changing the namespace of Class1InLibraryB to another sub-namespace 'guarantee' that there would be no naming conflict? Even if the full type name was Ploeh.Samples.LibraryB.Class1InLibraryB, I could still define a type in LibraryA called Ploeh.Samples.LibraryC.Class1InLibraryC...Roundhouse
You're right, that's the soft spot in my argumentation. The behavior has to do with the namespace analysis the compiler has to do to find Class1InLibraryA and verify it doesn't break CLS compliance. The namespace uncertainty maybe specific to C# (and Visual Basic.NET), since there are no means in the languages of specifying the assembly in a static type reference. However, I guess, due to this, the compilers take namespace shortcuts to verify CLS Rule 5. The point here is that namespaces are a language feature, not something the CLS requires.Neeley
@henbo: Thanks for your explanation - I had never really thought about namespaces being a language feature. I'm still not sure I get it, though, but I have to think it through a bit, I guess...Roundhouse
U
7

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.

Unreel answered 10/8, 2009 at 18:53 Comment(11)
Yes, but no types in LibraryC reference types in LibraryA, so I still don't understand why a reference to LibraryA is needed.Roundhouse
Yes, but LibraryC references LibraryB, which references LibraryA...so in order for the compiler to figure out that something in LibraryB is truely CLS-compliant it must also interrogate LibraryA and in order for it to do that, LibraryA must be included as a reference.Unreel
Remember, you are marking the entire assembly as being CLS-compliant, not just specific types. Since the entire assembly is marked as being CLS-compliant the compiler must verify all of the public types.Unreel
@Scott Dorman: Thanks your for your explanation. You may be right that this is how it is: It just doesn't make a whole lot of sense to me, because as soon as I change the namespace of Class1InLibraryB, the project compiles without error. In this case, LibraryC has no reference to LibraryA, but the compiler can none the less verify CLS Compliance.Roundhouse
@Mark Seemann: Updated my answer to address your last comment.Unreel
@Scott Dorman: I'm sorry I'm being so dim, but I still don't understand it. I can change the namespace for Class1InLibraryA to 'SomethingCompletelyDifferent' and the compiler error still occurs. In this case, Class1InLibraryA has no relation to the namespaces of the other types.Roundhouse
@Mark Seemann: No worries. CLS-compliance and how it relates to compilation isn't an easy thing to understand. I've updated my answer again which hopefully will make things clearer for you.Unreel
@Scott Dorman: Here's a new experiment: 'Fix' Class1InLibraryB so that it lives in the Ploeh.Samples.LibraryB namespace. The solution now compiles. Now add a new class to LibraryA called Ploeh.Samples.LibraryC.Class1InLibraryC - that is: the exact same name as the original Class1InLibraryC. The solution still compiles, even though that indidicates to me a potential naming conflict. If that is allowed without the compiler needing a reference from LibraryC to LibraryA, why does it need the reference when Class1InLibraryB sits in the Ploeh.Samples namespace?Roundhouse
@Mark Seemann: I'm not sure what you're doing here. In order to add the exact same class (which inherits from Class2InLibraryB) you need a reference in LibraryA to LibraryB which would create a circular dependency and Visual Studio won't allow you to add it.Unreel
@Scott Dorman: That's not what I'm saying: What I'm saying is that you can add a class to LibraryA that has the same Full Name (not Assembly Qualified Name, obviously) as a class in LibraryC, and both will still compile even with the CLSCompliant attribute set to true. As far as I can see, that goes against the argument that the compiler needs access to LibraryA to verify that naming is CLS compliant.Roundhouse
@Mark Seemann: Even though the class name is the same, remember that type names are always fully qualified for the compiler, so it does actually see these as two different types.Unreel
P
1

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.

Perorate answered 24/7, 2014 at 7:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.