Why CLS Compliance Matters for Library Authors
If you've ever published a NuGet package only to get complaints that it doesn't work from F# or VB.NET, you've hit a CLS compliance issue. The Common Language Specification defines a subset of .NET features that all languages must support. When your public API stays within these rules, any .NET language can consume your library without friction.
C# offers features like unsigned integers, operator overloading, and case-sensitive naming that not all .NET languages support. Using these in public APIs breaks interoperability. A VB.NET developer can't call a method with uint parameters because VB.NET doesn't have unsigned types. An F# consumer struggles with overloaded operators that F# handles differently.
You'll learn which C# features violate CLS compliance, how to mark your assembly for compliance checking, and patterns for exposing advanced features without breaking interoperability. By the end, you'll write libraries that work seamlessly across the entire .NET ecosystem.
Understanding CLS Compliance Rules
The CLS defines which types and language features are guaranteed to work across all .NET languages. Signed integers, strings, classes, and interfaces are compliant. Unsigned integers, pointers, and certain advanced generics are not.
Only public and protected members need compliance. Private implementation details can use any C# feature. This means you can use uint internally while exposing int in your public API. The compiler only checks members visible outside your assembly.
Compliance checking happens at compile time when you mark your assembly with the CLSCompliant attribute. The compiler emits warnings for any violations in public APIs. Fix these warnings before publishing to ensure cross-language compatibility.
using System;
// Mark entire assembly as CLS-compliant
[assembly: CLSCompliant(true)]
namespace DataLibrary;
// This class follows CLS rules
public class DataProcessor
{
// Compliant: uses signed integer
public int ProcessRecords(int count)
{
return count * 2;
}
// Non-compliant: uint is not CLS-compliant
// Compiler warning CS3001
public uint GetMaxSize()
{
return uint.MaxValue;
}
// Compliant: internal members can use any type
internal uint InternalSize { get; set; }
// Compliant: private implementation unrestricted
private ulong CalculateHash()
{
return 12345UL;
}
}
The assembly-level attribute enables compliance checking for all public types. The ProcessRecords method is compliant because int works in all .NET languages. GetMaxSize triggers a warning because uint isn't universally supported. Internal and private members can use uint freely since they're not part of the public contract.
Common CLS Violations and How to Fix Them
Several common C# patterns break CLS compliance. Knowing these helps you design better public APIs from the start rather than refactoring later.
Unsigned types are the most frequent violation. Replace uint with int, ulong with long, and ushort with short in public signatures. If you need the full range of unsigned types, consider exposing signed types and documenting valid ranges, or mark specific members as non-compliant.
Case-only naming differences cause problems too. You can't have public members that differ only by case like GetData and getData. Some languages are case-insensitive, so these appear as duplicate members. Use distinct names that work regardless of case sensitivity.
using System;
[assembly: CLSCompliant(true)]
namespace ComplianceDemo;
// Violation: case-only difference
public class BadNaming
{
public void ProcessData() { }
// CS3005: Different only in case
public void processData() { }
}
// Fixed: distinct names
public class GoodNaming
{
public void ProcessData() { }
public void ProcessDataAsync() { }
}
// Violation: unsigned in public API
public class BadTypes
{
public uint GetSize() => 100;
public ulong GetId() => 12345;
}
// Fixed: signed types with validation
public class GoodTypes
{
public int GetSize()
{
return 100;
}
public long GetId()
{
return 12345;
}
// Alternative: mark as non-compliant explicitly
[CLSCompliant(false)]
public uint GetSizeUnsigned() => 100;
}
The BadNaming class has methods differing only in case, which fails in case-insensitive languages. GoodNaming uses suffix patterns like Async to create distinct names. BadTypes exposes unsigned integers that VB.NET and F# can't handle naturally. GoodTypes uses signed types or explicitly marks non-compliant methods, letting consumers choose the appropriate overload.
Handling Generics and Advanced Scenarios
Generic types and constraints add complexity to CLS compliance. The rules require that all generic parameters and constraints use CLS-compliant types. You can't constrain to uint or expose List<uint> in public APIs.
Nested generic constraints work if all types are compliant. You can use IEnumerable<string> or Dictionary<int, string> because the generic parameters are compliant types. Complex scenarios like where T : SomeClass work when SomeClass itself is compliant.
Overloaded methods need careful design. You can overload by parameter count or type, but all overloads must use compliant types. Avoid overloading that differs only by signed versus unsigned variants like Process(int x) and Process(uint x), since the unsigned version breaks compliance.
using System;
using System.Collections.Generic;
[assembly: CLSCompliant(true)]
namespace GenericCompliance;
// Compliant generic class
public class Repository where T : class
{
private readonly List _items = new();
public void Add(T item)
{
_items.Add(item);
}
public IEnumerable GetAll()
{
return _items;
}
// Compliant: nested generics with compliant types
public Dictionary GetIndexed()
{
var dict = new Dictionary();
for (int i = 0; i < _items.Count; i++)
{
dict[i] = _items[i];
}
return dict;
}
}
// Non-compliant: constraint uses non-compliant type
public class BadRepository where T : IComparable
{
// CS3019: CLS compliance checking will not be performed
}
// Compliant alternative
public class GoodRepository where T : IComparable
{
public void Process(T item)
{
// Implementation
}
}
The Repository<T> class is fully compliant because T is constrained to class and all exposed types are compliant. GetIndexed returns Dictionary<int, T> which works because both int and T are compliant. BadRepository fails because IComparable<uint> uses an unsigned type. GoodRepository fixes this by constraining to IComparable<int> instead.
Verifying Compliance in Your Projects
The compiler is your first verification tool. Enable warnings as errors in your .csproj to catch violations during build. The CS3000-series warnings specifically indicate CLS compliance issues.
For libraries you consume, check their metadata. Reflection lets you query the CLSCompliant attribute on assemblies and types. This helps verify third-party dependencies before integrating them into your compliant library.
Testing with multiple languages provides real-world validation. Create small test projects in F# and VB.NET that reference your library. If they can't call your public APIs naturally, you've likely got compliance issues the compiler didn't catch.
using System;
using System.Reflection;
public class ComplianceChecker
{
public static void CheckAssembly(Assembly assembly)
{
// Check assembly-level compliance
var clsAttr = assembly.GetCustomAttribute();
if (clsAttr != null)
{
Console.WriteLine($"Assembly: {assembly.GetName().Name}");
Console.WriteLine($"CLS Compliant: {clsAttr.IsCompliant}");
}
else
{
Console.WriteLine("No CLS compliance declaration");
}
// Check public types
var publicTypes = assembly.GetExportedTypes();
Console.WriteLine($"\nPublic Types: {publicTypes.Length}");
foreach (var type in publicTypes)
{
var typeClsAttr = type.GetCustomAttribute();
if (typeClsAttr != null && !typeClsAttr.IsCompliant)
{
Console.WriteLine($" Non-compliant: {type.Name}");
}
}
}
}
This checker uses reflection to inspect assemblies at runtime. It reads the CLSCompliant attribute from the assembly and individual types. GetExportedTypes returns only public types, matching what external consumers see. You can extend this to check methods and properties for non-compliant members marked with CLSCompliant(false).
Mistakes to Avoid
Publishing without the CLSCompliant attribute: Without the assembly-level attribute, the compiler won't warn about violations. You might ship a library with unsigned types in public APIs and only discover the problem when F# users can't consume it. Add [assembly: CLSCompliant(true)] to every library you publish, even if you think you're compliant.
Assuming internal code needs compliance: Only public and protected members require CLS compliance. You can use pointers, unsigned types, and advanced C# features freely in private implementation. Restricting internal code unnecessarily limits your design options without any interoperability benefit.
Ignoring compiler warnings: CS3001, CS3002, and related warnings specifically flag CLS violations. Treat these as errors in library projects by adding <WarningsAsErrors>CS3001;CS3002;CS3003</WarningsAsErrors> to your .csproj. This prevents accidental violations from reaching consumers.
Try It Yourself: CLS Compliance Checker
Build a tool that analyzes assemblies for CLS compliance and reports violations. You'll inspect types, methods, and properties to identify non-compliant members.
Steps
- Create project:
dotnet new console -n ClsChecker
- Navigate:
cd ClsChecker
- Replace Program.cs with code below
- Update ClsChecker.csproj
- Execute:
dotnet run
using System.Reflection;
[assembly: CLSCompliant(true)]
// Sample compliant class
public class DataService
{
public int ProcessCount(int count) => count * 2;
[CLSCompliant(false)]
public uint GetMaxUnsigned() => uint.MaxValue;
}
// Check this assembly
var assembly = Assembly.GetExecutingAssembly();
var clsAttr = assembly.GetCustomAttribute();
Console.WriteLine("=== CLS Compliance Report ===\n");
Console.WriteLine($"Assembly: {assembly.GetName().Name}");
Console.WriteLine($"Declared Compliant: {clsAttr?.IsCompliant ?? false}\n");
var types = assembly.GetExportedTypes();
Console.WriteLine($"Public Types Analyzed: {types.Length}\n");
foreach (var type in types)
{
Console.WriteLine($"Type: {type.Name}");
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance |
BindingFlags.Static | BindingFlags.DeclaredOnly);
foreach (var method in methods)
{
var methodCls = method.GetCustomAttribute();
string status = methodCls?.IsCompliant == false ? "[Non-Compliant]" : "[OK]";
Console.WriteLine($" {status} {method.Name}");
}
}
Console.WriteLine("\n=== Analysis Complete ===");
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Run result
=== CLS Compliance Report ===
Assembly: ClsChecker
Declared Compliant: True
Public Types Analyzed: 2
Type: DataService
[OK] ProcessCount
[Non-Compliant] GetMaxUnsigned
Type: Program
[OK] <Main>$
=== Analysis Complete ===
When Not to Prioritize CLS Compliance
CLS compliance is valuable for public libraries, but it's not always the right choice. Knowing when to skip compliance lets you use C#'s full feature set where it matters.
Internal applications used by a single team don't need compliance if everyone uses C#. Use unsigned integers when they model your domain better, like for bit manipulation or protocol implementations. Restricting yourself to compliant types adds friction without any interoperability benefit.
Language-specific libraries targeting one platform can embrace that language's strengths. A C#-specific library can use tuples, pattern matching, and unsafe code without worrying about F# or VB.NET consumers. Document the language requirement clearly so users know what to expect.
Performance-critical code sometimes needs non-compliant features. Unsafe code with pointers, Span<byte> manipulation, and uint for optimal bit operations all violate CLS. Mark these members with CLSCompliant(false) and provide compliant alternatives when possible, letting callers choose between performance and portability.