📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the Common Language Specification (CLS), and when should you mark a library as CLSCompliant?

CLS rules for cross-language public APIs, the CLSCompliant attribute and the compiler warnings it enables.

Mid-Level Theory Intermediate clr cls interop library-design

🎯 Key points

  • The CLS is the subset of the CTS that all .NET languages are expected to support
  • Only public and protected members are checked; private and internal members are ignored
  • Unsigned types, case-only differences, leading underscores and pointers in public APIs are not compliant
  • [assembly: CLSCompliant(true)] enables compiler warnings such as CS3001, CS3002, CS3005 and CS3008
  • Use [CLSCompliant(false)] on specific members and provide a compliant alternative
  • Most valuable for shared libraries consumed by several languages

⏱ 30-second answer

The CLS is the subset of the CTS that every .NET language is expected to support, so a CLS-compliant public API can be used from C#, F#, VB and others. Typical rules: no unsigned types such as uint in public signatures, no public identifiers that differ only by case, no pointer types in public members. Adding [assembly: CLSCompliant(true)] makes the C# compiler warn (CS3001, CS3002, CS3005 and others) when a public member breaks the rules. Only the public surface matters; internal code can use anything.

🗣 Full answer (senior)

How I would answer this in an interview

The Common Language Specification (CLS) is a subset of the CTS — a set of rules for publicly visible API surface that every CLS-consuming .NET language is guaranteed to support. If a library's public API follows the CLS, any .NET language can call it. [assembly: CLSCompliant(true)] asks the C# compiler to check your public surface against those rules and warn when you break them.

1. Why it exists

.NET was designed for many languages with different capabilities: some are case-insensitive (VB.NET), some lacked unsigned integers, some don't support operator overloading or pointers. The CLS defines a common denominator so that public types and members of a library are consumable everywhere. Internal implementation can use anything.

2. Main CLS rules (for public/protected members)

  • No unsigned types in public signatures: uint, ulong, ushort, sbyte (use int, long, short, byte, or provide CLS-compliant alternatives).
  • No pointers or function pointers in public signatures.
  • Names must not differ only by case (Total and total both public) — case-insensitive languages couldn't distinguish them.
  • Identifiers must follow Unicode identifier rules; no leading underscores in some contexts (e.g. public members starting with _ trigger warnings).
  • Arrays must have zero-based lower bounds; element types must be CLS-compliant.
  • Overloads can't differ only by ref vs out or by unsigned vs signed parameter types.
  • Operator overloads should have named method alternatives (op_Addition exists, but languages without operator support need Add) — guidance rather than a hard rule.
  • Generic constraints and type parameters must be CLS-compliant; types deriving from non-compliant types are non-compliant.
  • Exceptions should derive from System.Exception.
  • Attributes used publicly should have CLS-compliant constructor parameter types.

3. How CLSCompliant works

[assembly: CLSCompliant(true)]

public sealed class Checksum
{
    public uint Compute(byte[] data) { ... }          // warning CS3002: return type not CLS-compliant

    [CLSCompliant(false)]
    public uint ComputeRaw(byte[] data) { ... }       // explicitly opted out: no warning
    public long Compute32(byte[] data) => ComputeRaw(data);   // CLS-friendly alternative
}
  • The attribute on the assembly turns on checking; individual members/types can opt out with [CLSCompliant(false)].
  • The compiler only checks — it doesn't change code generation. Non-compliant code still compiles (warnings CS3001–CS3027).
  • Non-public members aren't checked.
  • In newer .NET versions, many analysers and templates don't add CLSCompliant by default; the BCL itself is marked CLS-compliant with explicit opt-outs (e.g. UInt32 APIs).

4. When should you mark a library CLSCompliant?

Consider it when:

  • You publish a general-purpose public library (NuGet) that may be consumed from VB.NET, F#, PowerShell, or other .NET languages, and you want to guarantee broad usability.
  • Your organisation has mixed-language codebases (VB.NET legacy apps calling C# libraries).
  • You want a lint for case-only name differences and accidental unsigned types in public APIs.

Usually unnecessary when:

  • It's an application or internal library consumed only by C#.
  • The library is inherently low-level (interop, binary protocols) where unsigned types are natural — marking it compliant would require many opt-outs.
  • Consumers are known (e.g. microservice internals).

In practice, the most valuable parts of the CLS rules are good API design anyway: avoid case-only differences, prefer signed types unless the domain needs unsigned, provide named alternatives to operators.

5. A production example

A C# SDK we published for partners was also used by a partner's VB.NET desktop application. A release added two public properties differing only by case (Url and URL, one marked obsolete) and a method returning ulong. VB.NET consumers got ambiguity errors and couldn't use the method naturally. We added [assembly: CLSCompliant(true)], fixed the case clash, added long-returning alternatives with [CLSCompliant(false)] on the unsigned versions, and included a VB.NET smoke-test project in CI.

🔍 What the interviewer is checking

The interviewer checks you know the difference between CTS and CLS and when compliance matters.

Green flags: subset of CTS for cross-language use, public surface only, examples like unsigned types and case-only names, [CLSCompliant] usage with exemptions.

Red flags: confusing CLS with CLR; saying all .NET code must be CLS-compliant.

📚 Learn in depth

1. The CLS rule set

The CLS is defined in ECMA-335 Partition I (about 48 rules). Roles:

  • CLS framework: a library whose public API is compliant.
  • CLS consumer: a language that can use any CLS-compliant framework.
  • CLS extender: a language that can also define/extend CLS-compliant types (e.g. inherit, implement).

C#, VB.NET and F# are all consumers and extenders, and support most non-CLS features too.

2. Compiler warnings

Warning Meaning
CS3001/CS3002/CS3003 Argument/return/field type not CLS-compliant
CS3005 Identifiers differing only in case
CS3006 Overloads differing only in ref/out or array rank
CS3008 Identifier beginning with underscore
CS3009/CS3010 Base type/interface member not compliant
CS3014 Marking member compliant in a non-compliant assembly
CS3021 Unnecessary CLSCompliant(false) in non-compliant assembly

3. Unsigned types and modern APIs

Modern .NET uses unsigned types more (e.g. nuint, UInt128, BinaryPrimitives, IUnsignedNumber<T>). The BCL marks such APIs [CLSCompliant(false)]. Libraries focusing on systems programming often skip CLS compliance; general-purpose business libraries can comply easily.

4. CLS and language interop beyond types

CLS doesn't cover everything that affects usability: default interface methods, init-only setters (modreq makes them unusable from older compilers), required members, ref structs and in parameters may not be consumable in all languages. When targeting other languages, test with them.

Try it yourself

  1. Add [assembly: CLSCompliant(true)] to a library and expose a uint property; read the warning.
  2. Add two public members differing only by case.
  3. Consume the library from a VB.NET project.
  4. Mark a method [CLSCompliant(false)] and add a compliant alternative.

💻 Code

// In any file of the library, usually AssemblyInfo.cs or Directory.Build.props via <AssemblyAttribute>
[assembly: System.CLSCompliant(true)]

namespace Shared.Metrics;

public sealed class Counter
{
    private ulong _value;   // fine: private members are not checked

    // CLS-compliant surface: signed types only
    public long Value => (long)_value;

    public void Add(long amount) => _value += (ulong)amount;

    // Kept for C# callers, explicitly exempted
    [System.CLSCompliant(false)]
    public void Add(ulong amount) => _value += amount;
}

In an SDK-style project you can also add the attribute without a source file:

<ItemGroup>
  <AssemblyAttribute Include="System.CLSCompliantAttribute">
    <_Parameter1>true</_Parameter1>
    <_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
  </AssemblyAttribute>
</ItemGroup>

⚠ Common mistakes

1. Thinking CLSCompliant changes code

What happens: expecting the attribute to convert types or restrict usage at run time. Why it is wrong: it only enables compiler checks. Do instead: treat it as a lint for public APIs.

2. Marking applications CLS-compliant

What happens: adding the attribute to apps or internal libraries. Why it is wrong: noise without benefit. Do instead: use it for public, multi-language libraries.

3. Case-only differences in public APIs

What happens: Url and URL properties. Why it is wrong: unusable from case-insensitive languages; confusing in C# too. Do instead: distinct names.

4. Unsigned types in public APIs without alternatives

What happens: uint everywhere in a business library. Why it is wrong: awkward for some consumers; overflow surprises. Do instead: signed types unless the domain requires unsigned.

5. Suppressing warnings instead of fixing

What happens: #pragma warning disable CS3002 broadly. Why it is wrong: defeats the purpose. Do instead: fix or mark specific members [CLSCompliant(false)] with alternatives.

6. Assuming CLS guarantees full interop

What happens: expecting every modern C# feature to work in other languages. Why it is wrong: features like required or ref structs may not be supported. Do instead: test consumption from target languages.

7. Operators without named alternatives

What happens: public operator overloads only. Why it is wrong: languages without operator support can't use them easily. Do instead: add Add, Subtract methods (CA2225).

8. Public members starting with underscores

What happens: _internalHelper made public. Why it is wrong: CLS warnings and poor naming. Do instead: follow .NET naming guidelines.

🏛 Senior insight: trade-offs

A senior answer is pragmatic: the CLS is a tool for shared libraries, not a rule for everything.

Library type CLS compliance worth it?
Public NuGet SDK Yes, broad consumer base
Library used by VB.NET or F# teams Yes
Internal ASP.NET Core service Usually no
Performance library using uint and pointers Mark specific members non-compliant

The interesting judgement is designing an API that is both pleasant in C# and usable elsewhere, for example long overloads next to ulong ones.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says the CLS is a subset Why: Checks they can describe the full CTS it is a subset of
    How to answer

    The CTS is the type model defined by ECMA-335 that every .NET language compiles to. It defines value types and reference types, the members a type can have (fields, methods, properties, events, nested types), accessibility, single class inheritance with multiple interfaces, and that everything ultimately derives from System.Object. C# int is just an alias for the CTS type System.Int32, which is why a C# library can be used from F# or VB without conversion.

  2. When: Candidate talks about shared libraries Why: Probes assembly identity and versioning of shared libraries
    How to answer

    An assembly is the unit of deployment, versioning and loading in .NET: a PE file (.dll or .exe) containing IL, metadata, a manifest and optional resources. Its identity is name, version, culture and public key token. In modern .NET there is no GAC, strong-name signatures are not validated at load, and the runtime accepts an assembly whose version is the same or higher than the one referenced, so binding redirects are not needed. Assembly versions still matter for compatibility and for what NuGet and the SDK place in deps.json.

🛠 Real-world problems: how you handle them

VB.NET consumer could not use a shared library's overloads
Situation
A shared metrics library was used by C# services and a VB.NET reporting tool.
Signals
The VB team reported ambiguous method errors and could not call methods taking `ulong`.
Investigation
Turned on `[assembly: CLSCompliant(true)]` and the build listed CS3001 and CS3005 warnings for the offending members.
Resolution
Renamed case-only duplicates, added `long` overloads and marked `ulong` members `[CLSCompliant(false)]`.
Prevention
Kept the attribute on and treated CLS warnings as errors for that library.

How to tell it (STAR)

Our [metrics library] was also used by [a VB.NET reporting tool]. I enabled CLSCompliant, which flagged [N] public members. I added signed overloads and fixed case-only names, so the VB team could upgrade without workarounds.

↩ Usually asked after

More in CLR Fundamentals

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me