The Foundation of .NET Type Safety
The Common Type System (CTS) is the backbone of .NET's unified architecture. It defines how types work at the runtime level, ensuring that C# code can seamlessly interact with F#, VB.NET, or any other .NET language. This unified approach means you can use a library written in any .NET language from any other .NET language.
CTS provides the rules for declaring, using, and managing types in the runtime. It specifies how value types and reference types behave, how types inherit, and what members types can contain. Understanding CTS helps you write better code because you'll know how the runtime actually handles your types.
You'll learn how CTS organizes types, why everything inherits from System.Object, how the type system ensures safety, and what makes .NET's cross-language support work so smoothly.
Categories of Types in CTS
CTS divides all types into two fundamental categories: value types and reference types. Value types store their data directly, while reference types store a reference to their data on the heap. This distinction affects memory allocation, copying behavior, and performance characteristics.
// All types inherit from System.Object
// System.Object is the root of the type hierarchy
// Value Types - inherit from System.ValueType
// 1. Structs (custom value types)
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// 2. Enumerations
public enum Status
{
Pending,
Active,
Completed
}
// 3. Built-in value types
int number = 42; // System.Int32
double price = 19.99; // System.Double
bool isValid = true; // System.Boolean
decimal amount = 100.50m; // System.Decimal
// Reference Types - inherit directly from System.Object
// 1. Classes
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// 2. Interfaces
public interface IRepository
{
void Save(object entity);
}
// 3. Arrays
int[] numbers = { 1, 2, 3, 4, 5 };
string[] names = { "Alice", "Bob" };
// 4. Delegates
public delegate void NotifyHandler(string message);
// 5. Built-in reference types
string text = "Hello"; // System.String
object obj = new object(); // System.Object
// Demonstrating the unified type system
void DemonstrateUnification()
{
// Every type can be treated as object
object valueType = 42; // Boxing int to object
object referenceType = "text"; // Already a reference
object structType = new Point(); // Boxing struct
// All types have common methods from System.Object
Console.WriteLine(valueType.ToString());
Console.WriteLine(valueType.GetType());
Console.WriteLine(valueType.GetHashCode());
}
This unified hierarchy means every type in .NET shares common behavior. You can call ToString(), GetType(), and other System.Object methods on any value, whether it's a simple integer or a complex class instance.
Type Members Defined by CTS
CTS specifies what kinds of members a type can contain. These include fields, properties, methods, events, constructors, and nested types. The specification also defines access modifiers and how members can be declared and used across different languages.
public class Product
{
// Fields - store data directly
private decimal price;
private string name;
// Constants - compile-time values
public const decimal MaxDiscount = 0.5m;
// Properties - controlled access to data
public string Name
{
get => name;
set => name = value ?? throw new ArgumentNullException(nameof(value));
}
public decimal Price
{
get => price;
set
{
if (value < 0)
throw new ArgumentException("Price cannot be negative");
price = value;
}
}
// Auto-implemented property
public string Category { get; set; }
// Constructors - initialize instances
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
// Methods - define behavior
public decimal CalculateDiscountedPrice(decimal discountPercent)
{
if (discountPercent > MaxDiscount)
throw new ArgumentException($"Discount cannot exceed {MaxDiscount:P0}");
return Price * (1 - discountPercent);
}
// Static methods - operate on the type level
public static Product CreateSample()
{
return new Product("Sample Product", 9.99m)
{
Category = "Sample"
};
}
// Events - enable notification patterns
public event EventHandler PriceChanged;
protected virtual void OnPriceChanged()
{
PriceChanged?.Invoke(this, EventArgs.Empty);
}
// Indexers - array-like access
private Dictionary metadata = new();
public string this[string key]
{
get => metadata.TryGetValue(key, out var value) ? value : null;
set => metadata[key] = value;
}
// Nested type
public class PriceHistory
{
public DateTime Date { get; set; }
public decimal Price { get; set; }
}
}
// Using all member types
void DemonstrateMemberUsage()
{
var product = new Product("Laptop", 999.99m)
{
Category = "Electronics"
};
// Access property
Console.WriteLine($"Product: {product.Name}");
// Call method
decimal salePrice = product.CalculateDiscountedPrice(0.2m);
// Subscribe to event
product.PriceChanged += (sender, e) =>
Console.WriteLine("Price changed!");
// Use indexer
product["manufacturer"] = "TechCorp";
Console.WriteLine(product["manufacturer"]);
// Static method
var sample = Product.CreateSample();
}
CTS ensures all .NET languages support these member types. Even if the syntax differs between C# and F#, they compile to the same underlying member types that the CLR understands.
Cross-Language Interoperability
The real power of CTS shows up when you use libraries across different .NET languages. A class written in C# looks like any other class when used from F# because they both compile to CTS-compliant types.
// Library written in C#
namespace SharedLibrary;
public class Calculator
{
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
public int Multiply(int a, int b) => a * b;
public double Divide(int a, int b)
{
if (b == 0)
throw new DivideByZeroException();
return (double)a / b;
}
}
public interface ILogger
{
void Log(string message);
void LogError(string error);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[INFO] {message}");
}
public void LogError(string error)
{
Console.WriteLine($"[ERROR] {error}");
}
}
// Generic type that works across all .NET languages
public class Result
{
public bool Success { get; }
public T Value { get; }
public string Error { get; }
private Result(bool success, T value, string error)
{
Success = success;
Value = value;
Error = error;
}
public static Result Ok(T value) =>
new Result(true, value, null);
public static Result Fail(string error) =>
new Result(false, default, error);
}
// C# code using the library
using SharedLibrary;
var calculator = new Calculator();
int sum = calculator.Add(10, 20);
Console.WriteLine($"Sum: {sum}");
ILogger logger = new ConsoleLogger();
logger.Log("Application started");
Result<int> result = Result<int>.Ok(42);
if (result.Success)
{
Console.WriteLine($"Value: {result.Value}");
}
// This same library can be used from F#, VB.NET, or any .NET language
// The compiled IL is language-agnostic
Because CTS defines standard types, the C# library compiles to IL that any .NET language can consume. The type information in the assembly metadata follows CTS rules, so the F# or VB.NET compiler knows exactly how to use it.
Common Language Specification
While CTS defines all possible types, the Common Language Specification (CLS) defines a subset that all .NET languages must support. If you want maximum interoperability, follow CLS rules. This means avoiding language-specific features that other languages might not support.
// Mark assembly as CLS-compliant to get compiler warnings
[assembly: CLSCompliant(true)]
namespace MyLibrary;
// CLS-compliant: uses supported types and conventions
public class DataProcessor
{
// Good: uses CLS-compliant types
public string ProcessData(string input, int maxLength)
{
return input.Length > maxLength
? input.Substring(0, maxLength)
: input;
}
// Good: follows CLS naming rules
public void CalculateTotal(decimal[] prices)
{
// Implementation
}
}
// Non-CLS compliant examples (compiler will warn)
public class NonCompliantExamples
{
// Bad: unsigned types aren't CLS-compliant
// public uint GetCount() { return 0; }
// Bad: case-sensitive overloading isn't CLS-compliant
// public void Process(string data) { }
// public void process(string data) { }
// Bad: out parameters should be avoided for CLS compliance
// Use return values or ref parameters instead
// public void GetValues(out int x, out int y) { }
// Better CLS-compliant alternative
public (int x, int y) GetValues()
{
return (10, 20);
}
}
// CLS-compliant public API
[CLSCompliant(true)]
public class PublicApi
{
// Internal implementation can use non-CLS types
[CLSCompliant(false)]
internal uint InternalCounter { get; set; }
// Public interface uses only CLS types
public int GetCount()
{
return (int)InternalCounter;
}
// CLS-compliant generic constraints
public T Find(T[] items, Predicate condition) where T : class
{
return Array.Find(items, condition);
}
}
CLS compliance matters when building libraries meant for broad consumption. Marking your assembly as CLS-compliant makes the compiler warn you about features that might not work across all .NET languages, helping you create truly universal components.