CLS rules for cross-language public APIs, the CLSCompliant attribute and the compiler warnings it enables.
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.
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.
.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.
uint, ulong, ushort, sbyte (use int, long, short, byte, or provide CLS-compliant alternatives).Total and total both public) — case-insensitive languages couldn't distinguish them._ trigger warnings).ref vs out or by unsigned vs signed parameter types.op_Addition exists, but languages without operator support need Add) — guidance rather than a hard rule.System.Exception.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
}
[CLSCompliant(false)].CLSCompliant by default; the BCL itself is marked CLS-compliant with explicit opt-outs (e.g. UInt32 APIs).CLSCompliant?Consider it when:
Usually unnecessary when:
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.
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.
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.
The CLS is defined in ECMA-335 Partition I (about 48 rules). Roles:
C#, VB.NET and F# are all consumers and extenders, and support most non-CLS features too.
| 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 |
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.
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.
[assembly: CLSCompliant(true)] to a library and expose a uint property; read the warning.[CLSCompliant(false)] and add a compliant alternative.// 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>
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.
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.
What happens: Url and URL properties.
Why it is wrong: unusable from case-insensitive languages; confusing in C# too.
Do instead: distinct names.
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.
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.
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.
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).
What happens: _internalHelper made public.
Why it is wrong: CLS warnings and poor naming.
Do instead: follow .NET naming guidelines.
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.
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.
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.
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.
Last updated 30 Oct 2026.